最近在一台安卓手机的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那么上升几乎就看不出来了。