深入理解Node.js中的事件循环(带例子和分析)

本文受:

(上)https://zhuanlan.zhihu.com/p/26229293

(下)https://zhuanlan.zhihu.com/p/26238030

这两篇知乎博文的启发,对事件循环机制进行分析与总结。

 

对于JavaScript中的单线程,拥有唯一的一个事件循环,事件循环就像是一个

while (true) {
    // 执行一些代码...
}

一样,不断地去执行函数调用栈中的代码。在JavaScript代码执行时,除了依靠函数调用栈来搞定函数的执行顺序外,还要依靠任务队列来搞定另外一些代码的执行,但最终,任务队列中的代码总是会放到调用栈中去执行。

 

下面说一下事件循环机制中的几个重要的内容:

  1. 一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
  2. 任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
  3. macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  4. micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)。
  5. setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
  6. 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
  7. 事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
  8. 其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

首先,我们通过一个稍微简单一点的例子去分析,后面熟悉了之后再上一个稍微复杂一点的例子。

// 为了方便理解,我以打印出来的字符作为当前的任务名称
setTimeout(function() {
    console.log('timeout1');
})

new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})

console.log('global1');

分析过程:

首先,事件循环从宏任务开始,这个宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈考完成,当遇到任务源时,则先分发到对应的队列中。

第二步:script任务的执行首先遇到了setTimeout,setTimeout作为一个宏任务,它将任务分发到它对应的宏任务队列中(假设为setTimeout队列)

第三步:script执行到Promise处,Promise构造函数中的第一个参数是在new的时候执行的,即在当前任务直接执行而不会被放入任务的队列当中,里面的参数进入函数调用栈中执行,for循环不会进入任何队列,因此代码会依次执行。后续的then()中的函数则会被分发到微任务中的Promise队列中去。此时打印了:promise1, promise2。

第四步:script继续往下执行, 到最后一句的时候输出global1,全局任务执行完毕。第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务。这时候微任务中只有Promise队列中的一个任务then,因此直接执行,结果输出then1,当然它的执行也是进入函数调用栈中执行的。

第五步:当所有的微任务都执行完毕之后,表示第一轮循环就结束了,这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务开始。这时候发现宏任务中只有setTimeout队列中还有一个timeout1的任务等待执行,因此直接执行。

这时候宏任务队列和微任务队列中都没有任务了,所以代码就不会再输出其他东西了。

所以输出顺序为:promise1, promise2, global1, then1, timeout1

 

上面这个简单例子分析完成后,我们开始分析稍微复杂点的例子:

console.log('golbal1');

setTimeout(function() {
    console.log('timeout1');
    process.nextTick(function() {
        console.log('timeout1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout1_promise');
        resolve();
    }).then(function() {
        console.log('timeout1_then')
    })
})

setImmediate(function() {
    console.log('immediate1');
    process.nextTick(function() {
        console.log('immediate1_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate1_promise');
        resolve();
    }).then(function() {
        console.log('immediate1_then')
    })
})

process.nextTick(function() {
    console.log('glob1_nextTick');
})
new Promise(function(resolve) {
    console.log('glob1_promise');
    resolve();
}).then(function() {
    console.log('glob1_then')
})

setTimeout(function() {
    console.log('timeout2');
    process.nextTick(function() {
        console.log('timeout2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('timeout2_promise');
        resolve();
    }).then(function() {
        console.log('timeout2_then')
    })
})

process.nextTick(function() {
    console.log('glob2_nextTick');
})
new Promise(function(resolve) {
    console.log('glob2_promise');
    resolve();
}).then(function() {
    console.log('glob2_then')
})

setImmediate(function() {
    console.log('immediate2');
    process.nextTick(function() {
        console.log('immediate2_nextTick');
    })
    new Promise(function(resolve) {
        console.log('immediate2_promise');
        resolve();
    }).then(function() {
        console.log('immediate2_then')
    })
})

分析过程:

第一步:首先宏任务script执行,全局入栈,输出global1

第二步:往下执行,遇到setTimeout,timeout1进入宏任务setTimeout队列中。

第三步:往下执行,遇到setImmediate,immediate1进入宏任务setImmediate队列中。

第四步:往下执行,遇到process.nextTick,是微任务,global1_nextTick进入微任务process_nextTick队列中。

第五步:往下执行,遇到Promise,Promise构造函数中的参数直接执行,因此输出global1_promise,global1_then进入微任务的Promise队列中。

第六步:往下执行,遇到setTimeout,timeout2进入宏任务setTimeout队列中。

第七步:往下执行,遇到process.nextTick,global2_nextTick进入微任务process_nextTick队列中。

第八步:往下执行,遇到Promise,构造函数中的参数直接执行,因此输出global2_promise,global2_then进入微任务的Promise队列中。

第九步:往下执行,遇到setImmediate,immediate2进入宏任务setImmediate队列中。

此时整个script中的代码就执行完成了,如下图所示。执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。就下来,将会执行所有的微任务队列中的任务。

其中,nextTick队列会比Promise的先执行,nextTick中的可执行任务执行完成之后,才会开始执行Promise队列中的任务。

 

第十步:global1_nextTick进入函数调用栈执行,输出global1_nextTick1,紧接着global2_nextTick进入调用栈执行,输出global2_nextTick

第十一步:轮到Promise队列中的执行,global1_then进入执行栈执行,输出global1_then,然后接着global2_then进入执行栈执行,输出global2_then

第十二步:此时微任务队列为空,所有可执行的微任务已执行完毕,表示这一轮循环已经结束了,下一轮循环继续从宏任务开始执行。这时候,从SetTimeout队列中开始弹出任务timeout1到执行栈中执行,输出timeout1。往下执行,遇到timeout1_nextTick,放入nextTick队列中。继续往下执行,遇到Promise,timeout1_promise进入执行栈执行,输出timeout1_promise,把timeout1_then放入Promise队列中。

第十三步:执行完上一步这一个宏任务之后,就又开始执行微任务队列中的所有微任务,此时微任务队列中nextTick队列和Promise队列中均有任务,先后输出timeput1_nextTick输出timeout1_then

第十四步:此时所有微任务队列为空,开始执行下一轮循环,timeout2进入执行栈执行,输出timeout2。往下执行遇到nextTick,timeout2_nextTick进入nextTick队列中,继续执行,遇到Promise,输出timeout2_promise,timeout2_then进入Promise队列中。此时这一个宏任务又执行完成。

第十五步:执行完上一步这一个宏任务之后,就又开始执行微任务队列中的所有微任务,此时微任务队列中nextTick队列和Promise队列中均有任务,先后输出timeput2_nextTick输出timeout2_then

第十六步:此时setTimeout队列为空,开始执行setImmediate队列中的宏任务。immediate1进入执行栈,输出immediate1,往下执行遇到nextTick,immediate1_nextTick放入nextTick队列中,继续往下执行,遇到Promise,输出immediate1_promise,将immediate1_then放入Promise队列。

第十七步:此时一个宏任务执行完成,开始执行所有的微任务,输出immediate1_nextTick输出immediate1_then

第十八步:微任务队列为空,又开始下一个循环,开始执行最后一个宏任务immediate2,输出immediate2,往下执行,遇到nextTick,将immediate2_nextTick放入nextTick队列中,继续往下执行,遇到Promise,输出immediate2_promise,将immediate2_then放入Promise队列中。此时最后的宏任务已执行完毕。

第十九步:宏任务执行完毕,开始执行所有的微任务。先后输出immediate_nextTick输出immediate2_then

第二十步:开始下一轮循环,此时宏任务队列为空,执行完成。

因此,最后的输出顺序是:

golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout1_nextTick
timeout1_then
timeout2
timeout2_promise
timeout2_nextTick
timeout2_then
immediate1
immediate1_promise
immediate1_nextTick
immediate1_then
immediate2
immediate2_promise
immediate2_nextTick
immediate2_then

可以自己对照着一步一步去分析,非常有助于理解事件循环中的机制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章