setTimeout的执行时机

1,008 阅读3分钟

setTimeout()方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。

let timer1 = setTimeout(function[, delay, arg1, arg2, ...]);
let timer2 = setTimeout(function[, delay]);
let timer3 = setTimeout(code[, delay]);

delay作为可选参数,意义是延迟的毫秒数,默认值为0,函数的调用会在该延迟之后发生。如果省略该参数,意味着“马上”执行,或者尽快执行。不管是哪种情况,实际的延迟时间可能会比期待的(delay毫秒数) 值

比如下面的代码:

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
/*
6
6
6
6
6
6
*/
// 打印出6个6的原因是console.log(i)在for循环结束后才被执行

有很多因素会导致setTimeout的回调函数执行比设定的预期值更久,MDN上列举了一些常见的原因。

这里的原因应该是:

定时器有可能因为当前页面(或者操作系统/浏览器本身)被其他任务占用导致延时。 需要被强调是, 直到调用 setTimeout()的主线程执行完其他任务之后,回调函数和代码段才能被执行。

...

尽管setTimeout 以0ms的延迟来调用函数,但这个任务已经被放入了队列中并且等待下一次执行;并不是立即执行;队列中的等待函数被调用之前,当前代码必须全部运行完毕,因此这里运行结果并非预想的那样。

将上面的代码稍微改动一下:

for(let i = 0; i<6; i++){
  setTimeout(()=>{
    console.log(i)
  },0)
}
/*
0
1
2
3
4
5
*/

方应航的知乎专栏中,给出了这样的解释:

  1. for( let i = 0; i< 5; i++) 这句话的圆括号之间,有一个隐藏的作用域
  2. for( let i = 0; i< 5; i++) { 循环体 } 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。
  3. 其他细节就不说了,太细碎了 也就是说上面的代码段2可以近似近似近似地理解为
// 代码段3
var liList = document.querySelectorAll('li') // 共5个li
for( let i=0; i<liList.length; i++){
  let i = 隐藏作用域中的i // 看这里看这里看这里
  liList[i].onclick = function(){
    console.log(i)
  }
}

那样的话,5 次循环,就会有 5 个不同的 i,console.log 出来的 i 当然也是不同的值。

再加上隐藏作用域里的 i,一共有 6 个 i。

这就是 MDN 加那句 let j = i 的原因:方便新人理解。

总得来说就是 let/const 在与 for 一起用时,会有一个 perIterationBindings 的概念(一种语法糖)。

MDN在文档中提供了这篇文章,作者 David Baron 给出了一种比setTimeout更短延迟的方案:

    // Only add setZeroTimeout to the window object, and hide everything
    // else in a closure.
    (function() {
        var timeouts = [];
        var messageName = "zero-timeout-message";

        // Like setTimeout, but only takes a function argument.  There's
        // no time argument (always zero) and no arguments (you have to
        // use a closure).
        function setZeroTimeout(fn) {
            timeouts.push(fn);
            window.postMessage(messageName, "*");
        }

        function handleMessage(event) {
            if (event.source == window && event.data == messageName) {
                event.stopPropagation();
                if (timeouts.length > 0) {
                    var fn = timeouts.shift();
                    fn();
                }
            }
        }

        window.addEventListener("message", handleMessage, true);

        // Add the one thing we want added to the window object.
        window.setZeroTimeout = setZeroTimeout;
    })();

但这并不能让我们一开始的代码输出012345。

如果setTimeout延时太久了,那么我们让for循环更慢一些呢?比如:

sleep = (ms) => {
  return new Promise(resolve => setTimeout(resolve, ms))
}

! async function(){
  let i = 0
  for(i = 0; i<6; await sleep(0),i++){
    setTimeout(()=>{
      console.log(i)
    })
  }
}()
/*
0
1
2
3
4
5
*/

虽然得到了想要的结果,但并不知道这有什么卵用......


参考自:

  1. window.setTimeout
  2. 我用了两个月的时间才理解 let
  3. setTimeout with a shorter delay