本文内容比较长,请见谅。如有评议,还请评论区指点,谢谢大家!
Node端的异步执行顺序如下
同步代码 > process.nextTick > Promise.then中的函数 > setTimeOut(0) 或 setImmediate
setTimeout (function () {
console.log ('setTimeout');
}, 0);
setImmediate (function () {
console.log ('setImmediate');
});
new Promise (function (resolve, reject) {
resolve ();
}).then (function () {
console.log ('promise.then');
});
process.nextTick (function () {
console.log ('next nick');
});
console.log ('同步代码');
输出
备注1: Promise接收的函数的同步问题(实验论证)
console.log ('我是同步代码');
new Promise (function (resolve, reject) {
console.log ('resolve前');
resolve ();
console.log ('resolve后');
}).then (function () {});
console.log ('我是同步代码');
备注2: setTimeOut(0) 或 setImmediate的执行顺序问题
这个问题比较复杂,可参考下面这篇文章
浏览器中,涉及的异步API有:Promise, setTomeOut,setImmediate
(其中setImmediate可以忽略不计,因为它只在egde和IE11才支持,没错,Chrome和火狐都是不支持的,所以当然也不建议使用)
执行顺序
Promise.then中的函数 > setTimeOut(0) 或 setImmediate
以下代码
setTimeout (function () {
console.log ('setTimeout');
}, 0);
setImmediate (function () {
console.log ('setImmediate');
});
new Promise (function (resolve, reject) {
resolve ();
}).then (function () {
console.log ('promise');
});
在edge浏览器中的测试结果为
我们上面讲述了不同的程序,它们的异步执行顺序的区别,其中我们发现,有的异步API执行快,而有的异步API执行慢,实际上,它们作为异步任务,被分成了宏任务和微任务两大阵营,同时整体表现出微任务执行快于宏任务的现象
在宏任务和微任务方面,Node和浏览器也是差异很大的,这是因为它们的底层实现不一样。具体原理会在下面讲解,下面先概述下两种环境下的task的差别
下面简单介绍下宏任务和微任务的阵营
(⚠️该概念定义可能存在争议,部分资料对Node中也做了宏任务和微任务的划分,而部分资料则只提出了微任务的概念,而没有涉及宏任务,本文遵从前者)
当然了,直接说宏任务的执行比微任务的解释也许太粗糙了,没办法解释很多具体的问题,比如:具体不同的宏任务之间的顺序问题,所以,要做进一步的判断,我们就要理解JS事件循环中的执行阶段,和队列相关的知识
浏览器的事件循环是在 HTML5 中定义的规范,而 Node 中则是由 libuv 库实现,这是它们在实现上的根本差别。也就是说,很多时候,他们的行为看起来很像,但event loop的内在实现却存在差别。
我们看下规范的定义,以下援引自HTML5规范草案
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.
“为了协调事件,用户交互,脚本,渲染,网络等,用户代理(浏览器)必须使用本节中描述的事件循环。每个代理都有一个关联的事件循环。”
也就是说,浏览器根据这个草案的规定,实现了事件循环,目的是用来协调浏览器的事件,交互和渲染的。
Node的事件循环基于libuv实现,libuv是Node.js的底层依赖,一个跨平台的异步IO库。分别通过windows平台下的IOCP和Unix 环境下的 libev实现跨平台的兼容。
实际上,虽然libuv作为Node的底层模块,一开始是为了Node而设计的,但是它被抽象了出来,并且不仅仅为Node服务,也服务于其他语言,例如,它也支持了julia等语言的实现(Julia 是一个面向科学计算的语言)
Node的任务队列总共6个:包括4个主队列(main queue)和两个中间队列(intermediate queue)
(⚠️上面这个论断我是根据相关资料推断的,如有不当请指正)
(此概念 由Deepal Jayasekara,一位德国Node开发者提出,即上面文章的作者)
Q1.计时器队列 (timer queue)
在计数器队列中,Node会在这里保存setTimeOut和setInterval添加的处理程序,所以处理到这个队列的时候,Node会在一堆计时器中检查有没有过期的计时器,如果过期了,就调用其这个计时器的回调函数。如果有多个计时器到期(设置了相同的到期时间),那么会根据设置的先后,按照顺序去执行它们。
从这里也可以看出,为什么我们总会强调setTimeOut和setInterval的时间误差。这是因为只有在该循环流程中,检查到“过期”了,才会对计时器进行处理
Q2.IO事件队列(IO events queue)
IO一般指的是和CPU以外的外部设备通信的工作,例如文件操作和TCP/UDP网络操作等。
Node依赖于底层模块libuv提供的异步IO的功能。在IO事件队列中,Node将处理所有待处理的I/O操作
Q3.即时队列 (immediate queue)
处理这个队列的时候,setImmediate设置的函数回调,会被依次调用
Q4.关闭事件处理程序(close handlers queue)
当处理到这个队列的时候,Node将会处理所有I / O事件处理程序
Q5.next ticks队列
保存process.nextTick调用形成的任务
Q6.其他微任务队列
保存Promise形成的任务
在一轮循环中,4个主队列,每处理完一个主队列,接着就要把两个中间队列处理一次, 我的理解是:一趟循环走下来, 4个主队列都各自被处理了一次,而2个中间队列则是被处理了4次。
图示如下
这个图可能说的不是很清楚,所以我整理了一下,如下所示:
(备注⚠️:此图只适用于Node11.0.0版本以前的情况! 对于Node11以后的队列执行流程,请参考下面一节)
浏览器中只分两种队列:
他们的处理顺序是
如下图所示
Node和浏览器的区别情况是:
吐槽:听话的Node.js
修改前后区别在于
我们可以看出,微任务的执行变得更迅速了,不再是跟在任务队列处理完后处理,而是在单个timer类回调(setTimeout,setImmediate)处理完后,也会被处理了。
让我们分析下面这段代码
setTimeout (function () {
console.log ('timeout1:宏任务');
new Promise (function (resolve, reject) {
resolve ();
}).then (() => {
console.log ('promise:微任务');
});
});
setTimeout (function () {
console.log ('timeout2:宏任务');
});
对这段代码
运行结果
浏览器
Node10.16.3(nvm切换node版本)
Node11.0.0(nvm切换node版本)
我们不难发现其中差别,Node10.16.3的表现是和浏览器不一样的,而到了Node11,则Node和浏览器相一致了。
(⚠️下面的是个人理解,如有您有更合理的观点,请在评论区给出,谢谢)
好吧,其实上面的内容已经有点复杂了! 可是这个时候,又有个神奇的概念过来插一脚 它就是,Node官方文档里面提出的“七队列”
下面介绍一下这位小伙伴
>> 七队列的具体作用
“HTML5 规范规定最小延迟时间不能小于 4ms,即 x 如果小于 4,会被当做 4 来处理。 不过不同浏览器的实现不一样,比如,Chrome 可以设置 1ms,IE11/Edge 是 4ms。”
一句话足以:Node端没有最小延迟时间
Node没有最小延迟,这实际上是浏览器和节点之间的兼容性问题。计时器(setTimeout和setImmediate)在JavaScript中是完全未指定的(这是DOM规范,在Node中没有用,何况浏览器也没有遵循),而node实现它们的原因仅仅是因为它们在JavaScript的历史上非常地基础
It doesn't have a minimum delay and this is actually a compatibility issue between browsers and node. Timers are completely unspecified in JavaScript (it's a DOM specification which has no use in Node and isn't even followed by browsers anyway) and node implements them simply due to how fundamental they've been in JavaScript's history
这个问题其实比较复杂,不能一概而论。
第一.在主线程中运行以下脚本,我们不能确定timeout和immediate输出的先后顺序,结果受到进程性能的影响 (例子源于Node官方文档,链接在下面给出)
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
结果
输出结果无法确定
第二.如果在一个IO循环中运行setTimeOut(0,function) 和setImmediate,那么setImmediate 总是被优先调用
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
输出结果
immediate timeout