最近在一臺安卓手機的webview裏面遇到一個神奇的問題,setTimeout不會觸發了。原由是筆者用了一個動畫庫,這個動畫庫調了它的初始化方法後沒有生成DOM元素,通過一番排查,最後發現是有一個地方的setTimeout回調沒有執行,以下圖所示:javascript
setTimeout有被執行到,可是它的回調始終沒有執行,我直接在控制檯上執行setTimeout也沒有效果,以下圖所示:java
這是爲啥呢?經檢驗setTimeout沒有被覆蓋,仍是原生的那個。這個的UA爲安卓8,以下圖所示:android
在網上搜羅一番,只在Stackoverflow找到一個相關的Q&A,可是沒有辦法解決,惟一能解決的方法是重啓APP或者重啓機器有時候就能夠了。那怎麼辦呢,難道只能坐以待斃,跟他們說這個問題是手機的bug,沒法解決?有沒有辦法hack一下web
試了下setInterval也是一樣現象,沒法觸發,推測可能事件循環有點混亂了。又試了下requestAnimationFrame能夠用,彷佛看到了曙光,這個也是另一種異步的機制,能夠在requestAnimationFrame裏面判斷時間是否接近設定的時間,若是是的話,那就執行回調,也就是說用requestAnimationFrame來polyfill setTimeout.數組
第一步,須要判斷一下setTimeout是否能運行,若是不能的話才進行覆蓋。怎麼判斷呢?天然是setTimeout裏面設置一個變量,若是設置生效說明能運行,以下代碼所示:閉包
let setTimeoutWork = false;
setTimeout(() => {
setTimeoutWork = true;
}, 0);複製代碼
接着第二步,polyfill須要在什麼時機判斷這個變量有沒有被設置成功?能夠在requestAnimationFrame裏面,以下代碼所示:異步
function hackSetTimeout() {
if (setTimeoutWork) {
return;
}
console.warn('setTimeout not work!');
}
window.requestAnimationFrame(hackSetTimeout);
複製代碼
按理說requestAnimationFrame應該會更慢於setTimeout 0,然鵝,咱們發現,這個requestAnimationFrame竟然比setTimeout 0更快執行,以下圖所示:函數
requestAnimationFrame是在0.3ms以後執行,而setTimeout是在1.1ms後執行的。而在火狐上結果是相反的:性能
這個多是由於Chrome認爲requestAnimationFrame比setTimeout 0擁有更高的優先級。無論怎麼樣,須要變一下,咱們能夠在第二次requestAnimationFrame的時候纔去判斷,以下代碼所示:優化
let time = 0;
function hackSetTimeout() {
// 等到第二次,setTimeout 0纔會執行
if (++time <= 1) {
window.requestAnimationFrame(hackSetTimeout);
return;
}
if (setTimeoutWork) {
return;
}
console.warn('setTimeout not work!');
}
window.requestAnimationFrame(hackSetTimeout);複製代碼
這個時候順序就對了,以下圖所示:
這個判斷須要很是謹慎,由於咱們不可以影響絕大多數正常的設備。
第三步對setTimeout進行覆蓋,以下代碼所示:
window.setTimeout = function(caller, time) {
let begin = Date.now();
window.requestAnimationFrame(function call() {
if (Date.now() - begin > time) {
caller();
} else {
window.requestAnimationFrame(call);
}
});
return 0;
};複製代碼
邏輯很簡單,就是利用閉包,設置一個beginTime,而後不斷地requestAnimationFrame,當時間到的時候便執行傳給setTimeout的回調,函數還要返回一個tId。
第四步,考慮clearTimeout如何實現,以下代碼所示:
let tId = 0;
let tIdCancelMap = {};
let tIdCallers = [];
window.clearTimeout = function(tId) {
tIdCancelMap[tId] = true;
};
window.setTimeout = function(caller, time) {
tIdCallers[++tId] = caller;
let begin = Date.now();
window.requestAnimationFrame(function call() {
let _tId = tIdCallers.indexOf(caller);
if (tIdCancelMap[_tId]) {
return;
}
if (Date.now() - begin > time) {
caller();
} else {
window.requestAnimationFrame(call);
}
});
return tId;
};複製代碼
如上代碼所示,用一個tIdCallers數組保存全部的setTimeout回調,數組的索引即是tId,而後再用一個Map記錄對應的tId有沒有被cancel,當requestAnimationFrame回調觸發執行的時候,先找一下caller所對應的tId,這個就是tIdCallers數組的做用,由於咱們要想辦法獲得對應的tId(注意這裏不能利用閉包裏的tId,由於它永遠是屢次調用後最後的那個值),而後在canleMap裏面看一下這個tId有沒有被canel了,若是是的話到此結束,不然才比較時間。
setInterval也是用一樣的方式,只是它在執行完回調後還要繼續註冊requestAnimationFrame,以下代碼所示:
window.clearInterval = function(tId) {
tIdCancelMap[tId] = true;
};
window.setInterval = function(caller, time) {
tIdCallers[++tId] = caller;
let begin = Date.now();
window.requestAnimationFrame(function call() {
let _tId = tIdCallers.indexOf(caller);
if (tIdCancelMap[_tId]) {
return;
}
if (Date.now() - begin > time) {
caller();
begin = Date.now();
window.requestAnimationFrame(call);
} else {
window.requestAnimationFrame(call);
}
});
return 0;
};複製代碼
這樣便解決了setTimeout回調不觸發的問題,可能時間沒有setTimeout的準,會稍微延後一點,能夠進一步優化,例如當時間差絕對值小於某個數如10ms的時候便認爲到時間了。在性能上也不會有太大的消耗,雖然requestAnimationFrame的觸發比較快,可是咱們裏面的操做很是少,經觀察,若是頁面是純靜態的,註冊了requestAnimationFrame會致使CPU上升到3% ~ 4%,而若是頁面自己已經註冊了requestAnimationFrame那麼上升幾乎就看不出來了。