3

先看一段代码,也是一道经典面试题:

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()

其输出结果为:

// 1
// 2
// 3
// 5
// 4


我们知道,JavaScript 在同一时间片内只能执行一个任务:

主线程会依次执行代码,当执行到函数的时候会将函数加入执行栈,当函数执行完毕后再将其出栈,直至代码执行完毕。当执行栈为空时,runtime 会从任务队列(先入先出)中取出待执行的回调函数并执行,入栈、出栈的过程同上。这个机制就叫做 Event Loop。

由此可以认为,Event Loop实际上是一系列的回调函数集合。

举例来说:在浏览器中,对于网络请求等需要等待一段时间才会返回结果的操作,我们通常采用异步回调来处理,这个回调就会放入任务队列中。此时浏览器会在其它线程中执行异步操作,操作完成后将回调函数放入主线程任务队列中。Event Loop负责在主线程执行完毕后将任务队列中的函数放入执行栈中,由主线程执行。当主线程将执行栈中的函数执行完毕后,再次读取任务队列,形成循环。所以即使主线程阻塞了,任务队列依然能够被添加函数,因为任务队列的添加是由浏览器负责的。(不同的 runtime 实现可能不同)

另外需要注意的是 Promise.then 是异步执行的,而创建 Promise 实例是同步执行的。这就解释了为什么1、2、3输出在4、5之前。

但为什么5 会输出在4前面呢?

JavaScript 中的任务又分为MacroTask 与 MicroTask 两种。

典型的 MacroTask 包含了 :

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

而常见的MicroTask 包含了

  • process.nextTick
  • Promises
  • Object.observe
  • MutationObserver

Event Loop 中有一个或多个Task Queue,即MacroTask Queue,仅有一个Job Queue,即MicroTask Queue。Task Queue的执行是按照回调顺序先入先出,而在 MacroTask 的执行间隙中会清空已有的 MicroTask Queue

回到代码中,setTimeout(function() {console.log(4)}, 0); 既然延迟设置为0,为什么5会在4之前输入呢?

那是因为setTimeout设置为0的时候,runtime其实并不是0,在主流浏览器中会将其设置为4,而 node 则会将其设置为1。那么现在代码的执行顺序就很清晰了:

console.log(1);    // 创建 Promise 主线程执行
...
console.log(2); // 创建 Promise 主线程执行

console.log(3); // test 函数立即执行, 主线程执行
... 
console.log(5); // 主线程执行完毕,执行MicroTask Queue,即 promise.then
... 
console.log(4);    // 执行 setTimeout(4)



参考资料:

https://developer.mozilla.org...

https://html.spec.whatwg.org/...

qr.001.jpeg


王亮hengg
456 声望1.1k 粉丝

资深拷贝工程师