偶然看了一篇文章讲了浏览器端与nodejs端的事件循环机制,但在检验的过程中发现了浏览器与nodejs两者之间存在不符合统一解释的差异,由此引出了一系列测试,记录一下。
在此之前,我理解的Browser的循环机制是下面这样的:
nodejs的循环机制是:
主线程去执行同步代码、异步非I/O代码 ,异步I/O代码则有主线程分配给线程池的子线程执行,执行完毕后将回调函数交给主线程执行。整个过程以事件循环机制连接;我只能强行解释到这了,如果还想再权威一下,可以到下面的链接看一下。
此图来自阮一峰的网络日志http://www.ruanyifeng.com/blog/2014/10/event-loop.html
这是之前所理解的事件循环机制,但今天看到的文章,从任务的角度统一了Browser与nodejs的循环机制(但是还是存在差异的),对于上下两种对机制的阐述,应该说下面的阐述是对事件队列的进一步拆分。
js语言为单线程,为了不使执行缓慢的任务阻塞后续任务的执行,所以将js的任务分为同步任务、异步任务;
同步任务在函数调用栈内执行,异步任务则需要借助任务队列,一个线程中调用栈只有一个,但任务队列可以有多个;
1、任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。
2、macro-task大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
3、micro-task大概包括: process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
4、setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
5、来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
6、事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
7、其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。
其中Browser与nodejs的差异就体现在第六条:
Browser端严格遵循第六条,执行完一个宏任务就会去检查微任务队列是否有需要执行的微任务,即使微任务内嵌套微任务,也会将嵌套的微任务执行完毕后(这点上nodejs与browser是相同的,对应的就是清空微任务的队列),再去宏任务队列执行下一个宏任务;
nodejs端则会将同源的任务放在一起执行,如果涉及到同源宏任务的嵌套,仍会将同源任务放在一起,但是内部的任务会放在下一次事件循环时执行。若还不理解可以看下面的图片:
这是在script整段代码执行完毕后,还未清空微任务队列时的图示,从图中可以看出相同任务源会放在一个队列执行,执行一个宏任务意味着将该队列执行完毕,当然除去嵌套内部的任务源,举个栗子,如果timeoutTwo中嵌套了一个timeoutThree,timeThree仍就会放在setTimeout队列内,但是不会与one、two一起被执行,而是等到下一次事件循环时才会执行;还有setInterval也会与setTimeout放在一起(因为他们算作相同的任务源,只是setTimeout执行一次,setInterval执行多次);
重点在于理解事件循环结束节点,一次宏任务执行完毕再将微任务队列清空为一个事件循环节点;
本文所举例子,只是设置setTimeout延时为0,它会立即进入任务队列,假如设置更高的延时,还有设置了setInterval则会更复杂,还会存在更多的不确定性,所以需要多次实验分析把原理搞透,才能在使用这些异步api时能够游刃有余;
本文测试所使用的的Browser为chrome 73.0.3683.75,nodejs版本为v8.12.0
最后再放一个栗子,将代码放在浏览器与nodejs中就会发现不同,这就是两个环境之间的差异
'use strict';
console.log(1);
setTimeout(() => {
console.log(2)
new Promise((resolve) => {
console.log(6);
resolve(7);
}).then((num) => {
console.log(num);
})
});
setTimeout(() => {
console.log(3);
new Promise((resolve) => {
console.log(9);
resolve(10);
}).then((num) => {
console.log(num);
})
setTimeout(()=>{
console.log(8);
})
})
new Promise((resolve) => {
console.log(4);
resolve(5)
}).then((num) => {
console.log(num);
new Promise((resolve)=>{
console.log(11);
resolve(12);
}).then((num)=>{
console.log(num);
})
})
运行结果如下