老铁,据说你的setTimeout不会触发,咋整

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

相关文章
相关标签/搜索