I recently encountered a magic problem in the WebView of an Android phone. SetTimeout does not trigger. The reason is that the author used an animation library, which did not generate DOM elements after calling its initialization method. After some investigation, it was found that the setTimeout callback was not executed in one place, as shown in the following figure:

SetTimeout was executed, but its callback was never executed, and it had no effect when I executed setTimeout directly on the console, as shown below:

Why is that? Check that setTimeout is not overridden and is still the native one. The UA of this is Android 8, as shown below:

Search the Internet and find only one related Q&A on Stackoverflow, but there is no solution. The only solution is to restart the APP or reboot the machine sometimes. So what, just sit back and tell them the problem is a phone bug that can’t be fixed? Is there a way to hack it

Try setInterval, the same phenomenon, failed to trigger, presumably the event loop is a bit chaotic. RequestAnimationFrame is another asynchronous mechanism that can be used in requestAnimationFrame to determine if the time is close to the specified time. If so, execute a callback. That is, polyfill setTimeout with requestAnimationFrame.

The first step is to determine if setTimeout works and override it if not. How do you tell? Set a variable in setTimeout. If the setting takes effect, it will work, as shown in the following code:

let setTimeoutWork = false;
setTimeout((a)= > {
  setTimeoutWork = true;
}, 0);Copy the code

Next, at what point does Polyfill need to determine if this variable has been set successfully? This can be done in requestAnimationFrame as follows:

function hackSetTimeout() {
  if (setTimeoutWork) {
    return;
  }
  console.warn('setTimeout not work! ');
}
windowRequestAnimationFrame (hackSetTimeout);Copy the code

RequestAnimationFrame should be slower than setTimeout 0. However, requestAnimationFrame should be faster than setTimeout 0.

RequestAnimationFrame is executed after 0.3ms and setTimeout after 1.1ms. On Firefox, the result is the opposite:

This is probably because Chrome thinks requestAnimationFrame has a higher priority than setTimeout 0. However, the need for a change can be determined by the second requestAnimationFrame, as shown in the following code:

let time = 0;
function hackSetTimeout() {
  // Wait until the second time, setTimeout 0 is executed
  if (++time <= 1) {
    window.requestAnimationFrame(hackSetTimeout);
    return;
  }
  if (setTimeoutWork) {
    return;
  }
  console.warn('setTimeout not work! ');
}
window.requestAnimationFrame(hackSetTimeout);Copy the code

At this time, the order is correct, as shown in the picture below:

This judgment needs to be made with great caution, as we cannot affect the vast majority of normal devices.

Step 3 Override setTimeout, as shown in the following code:

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;
};Copy the code

The logic is simple, just use the closure, set a beginTime, and keep requestAnimationFrame, execute the callback to setTimeout when the time is up, and return an tId.

Step 4, consider how clearTimeout is implemented, as shown in the following code:

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;
};Copy the code

As shown in the code above, use a tIdCallers array to store all setTimeout callbacks. The index of the array is tId, and then use a Map to record whether the tId has been cancelled. When the requestAnimationFrame callback is triggered, Let’s look at the tId that corresponds to our caller. This is what tIdCallers is for, because we need to find the tId that corresponds to our caller. Then check in the canleMap to see if the tId is canel, if so, end, otherwise compare the time.

SetInterval works the same way, except that it continues to register requestAnimationFrame after the callback, as shown below:

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;
};Copy the code

In this way, the setTimeout callback will not trigger the problem. Maybe the time is not as accurate as setTimeout and will be slightly delayed, which can be further optimized. For example, when the absolute value of time difference is less than a certain number, such as 10ms, the time is considered to be up. There is also no significant performance cost. Although requestAnimationFrame fires quickly, we have very few operations in it. It has been observed that registering requestAnimationFrame increases CPU by 3% ~ 4% if the page is purely static. If the page itself is already registered with requestAnimationFrame, the increase is barely noticeable.