JobPlus知识库 IT 软件开发 文章
通过microtasks和宏任务看JavaScript的异步任务执行顺序

初探--- setTimeout()的那些事儿

相信很多人在初学的JavaScript的时候都遇到过类似的代码:

   // part1    console.log(1);    setTimeout(function(){        console.log(2);    },100);    console.log(3);    console.log(4);    // part2    console.log(1);    setTimeout(function(){        console.log(2);    },0);    console.log(3);    console.log(4);

萌新们看到这个,肯定觉得很简单.part1输出顺序是1,3,4,2;第2部分输出顺序是1,2,3,4嘛显而易见,part1的中在打印2的时候延迟了100ms时,所以被放到了队列的尾端执行,理所当然的最后输出;第2部分中虽然调用了setTimeout的函数,但是延迟设置为0毫秒,实际上并未延迟,因此应该立即执行,所以输出顺序应该是1,2,3 4。

看到这里,很多人应该都知道了,上面的说法实际上是错误的。

的setTimeout(FN,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在 “任务队列” 的尾部添加一个事件,因此要等到同步任务和 “任务队列” 现有的事件都处理完,才会得到执行。

2.深入--- setTimeout()和Promise同场竞技时是什么样呢?

众所周知,无极是ES6发布的一种非常流行的异步实现方案,当无极和setTimeout的同时出现在一段代码中,他们的执行顺序是什么样子的呢请看下面这段代码?

   setTimeout(function(){        console.log(1)    },0);    new Promise(function(resolve){        console.log(2)        for( var i=100000 ; i>0 ; i-- ){            i==1 && resolve()        }        console.log(3)    }).then(function(){        console.log(4)    });    console.log(5);

如果按照正常逻辑分析,应该是这样的:

  1. 当运行到的setTimeout时,会把的setTimeout的回调函数的console.log(1)放到任务队列里去,然后继续向下执行。
  2. 接下来会遇到一个无极。首先执行打印的console.log(2),然后执行用于循环,即时用于循环要累加到10万,也是在执行栈里面,等待用于循环执行完毕以后,将承诺的状态从完成切换到的决心,随后把要执行的回调函数,也就是然后里面的的console.log(4)推到任务队列里面去。接下来马上执行马上的console.log(3)。
  3. 然后出无极,还剩一个同步的的console.log(5),直接打印。这样第一轮下来,已经依次打印了2,3,5。
  4. 然后再读取任务队列,任务队列里还剩的console.log(1)和执行console.log(4),因为任务队列是队列嘛,肯定遵循的先进先出的策略,因此更早入列的的setTimeout( )的回调函数先执行,打印1,最后剩下无极的回调,打印4。

因此一通分析下来,得到的打印结果是2,3,5,1,4。那我们实际试一下呢?

   2    3    5    4    1

啊嘞嘞?跟我们一开始想象的貌似有点不一样啊!是什么原因导致了原本应该在setTimeout的回调后面的承诺的回调反而跑到前面去执行了呢?

为了搞清这个问题,我专门去翻阅了一下资料,首先找到了承诺/ A +标准里面提到:

这里的“平台代码”是指引擎,环境和承诺的实现代码。在实践中,这个要求确保了onFulfilled和onRejected异步执行,在事件循环开始之后被调用,并且有一个新的堆栈。这可以使用诸如setTimeout或setImmediate之类的“宏任务”机制,或者使用诸如MutationObserver或process.nextTick的“微任务”机制来实现。由于承诺实现被视为平台代码,因此它本身可能包含一个任务调度队列或“蹦床”,其中调用了处理程序。

这里提到了微任务和宏任务这两个概念,并分别列举了两种情况:的setTimeout和setImmediate属性宏任务,MutationObserver和process.nextTick属性微任务但并没有进一步的详述,于是我以此为线索进一步搜索资料,找到计算器上的一个问答,终于让我的疑惑得到解决。

宏任务和microtasks的划分:

宏任务:

  • 的setTimeout
  • 的setInterval
  • setImmediate
  • requestAnimationFrame
  • I / O
  • UI渲染

microtasks:

  • process.nextTick
  • 承诺
  • Object.observe
  • MutationObserver

那我们上面提到的任务队列到底是什么呢?跟宏任务和microtasks有什么联系呢?

  • 事件循环有一个或多个任务队列(任务队列是macrotask队列)
  • 每个事件循环都有一个microtask队列。
  • 任务队列= macrotask队列!= microtask队列
  • 一个任务可能会被推入macrotask队列或microtask队列
  • 当一个任务被推入一个队列(微/宏)时,意味着准备工作已经完成,所以现在可以执行任务。

翻译一下就是:

  • 一个事件循环有一个或者多个任务队列;
  • 每个事件循环都有一个microtask队列
  • 宏任务队列就是我们常说的任务队列,microtask队列不是任务队列
  • 一个任务可以被放入到宏任务队列,也可以放入microtask队列
  • 当一个任务被放入microtask或者宏任务队列后,准备工作就已经结束,这时候可以开始执行任务了。

可见,setTimeout的和娓娓道来不是同一类的任务,处理方式应该会有区别,具体的处理方式有什么不同呢我从?这篇文章里找到了下面这段话教育:

微任务通常安排在当前正在执行的脚本之后直接发生的事情,比如对一批动作做出反应,或者做一些异步的事情,而不用承担整个新任务的惩罚。只要没有其他JavaScript在执行中,并且在每个任务结束时,microtask队列就会在回调之后处理。在微任务中排队的任何其他微任务将被添加到队列的末尾并进行处理。微任务包括突变观察者回调,正如在上面的例子中,承诺回调。

通俗的解释一下,microtasks的作用是用来调度应当在当前执行的脚本执行结束后立即执行的任务。   例如响应事件,或者异步操作,以避免付出额外的一个任务的费用。

microtask会在两种情况下执行:

  1. 任务队列(macrotask =任务队列)回调后执行,前提条件是当前没有其他执行中的代码。
  2. 每个任务的末尾执行。

另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。

也就是说执行顺序是:

开始 - > 取任务队列第一个任务执行 - >取microtask全部任务依次执行 - >取任务队列下一个任务执行 - >再次取出microtask全部任务执行 - > ...这样循环往复

一旦一个承诺落户,或者如果已经解决了,它就会排起一个微型任务来进行反动的回调。这确保承诺回调是异步的,即使承诺已经解决。所以,打电话问候,然后(yey,nay)反对已经解决的承诺,立刻排队等候一个微型任务。这就是为什么在脚本结束之后记录promise1和promise2的原因,因为在处理微任务之前,当前正在运行的脚本必须完成。promise1和promise2在setTimeout之前被记录,因为microtasks总是在下一个任务之前发生。

承诺一旦状态置为完成态,便为其回调(。然后内的函数)安排一个microtask。

接下来我们看回我们上面的代码

   setTimeout(function(){        console.log(1)    },0);    new Promise(function(resolve){        console.log(2)        for( var i=100000 ; i>0 ; i-- ){            i==1 && resolve()        }        console.log(3)    }).then(function(){        console.log(4)    });    console.log(5);

按照上面的规则重新分析一遍:

  1. 当运行到的setTimeout时,会把的setTimeout的回调函数的console.log(1)放到任务队列里去,然后继续向下执行。
  2. 接下来会遇到一个无极。首先执行打印的console.log(2),然后执行用于循环,即时用于循环要累加到10万,也是在执行栈里面,等待用于循环执行完毕以后,将承诺的状态从完成切换到的决心,随后把要执行的回调函数,也就是然后里面的的console.log(4)推到microtask里面去。接下来马上执行马上的console.log(3)。
  3. 然后出无极,还剩一个同步的的console.log(5),直接打印。这样第一轮下来,已经依次打印了2,3,5。
  4. 现在第一轮任务队列已经执行完毕,没有正在执行的代码。符合上面讲的microtask执行条件,因此会将microtask中的任务优先执行,因此执行的console.log(4)
  5. 最后还剩宏任务里的setTimeout的放入的函数的console.log(1)最后执行。

如此分析输出顺序是:

   2    3    5    4    1

看吧,这次分析对了呢ヾ(◍°∇°◍)ノ゙

3.总结和参考资料

microtask和macrotask看起来容易混淆,实际上还是很好区分的.macrotask就是我们常说的任务队列(task queue)。

JavaScript的执行顺序可以简要总结如下:

开始 - > 取任务队列第一个任务执行 - >取microtask全部任务依次执行 - >取任务队列下一个任务执行 - >再次取出microtask全部任务执行 - > ...



如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持
304人赞 举报
分享到
用户评价(0)

暂无评价,你也可以发布评价哦:)

扫码APP

扫描使用APP

扫码使用

扫描使用小程序