前言
>> 目录
-
开门见山:Node和浏览器的异步执行顺序问题
-
两种环境下的宏任务和微任务(macrotask && microtask)
-
Node和浏览器的事件循环模型在实现层面的区别
-
Node和浏览器的事件循环的任务队列(task queue)
-
Node和浏览器的事件循环模型在表现层面的差异
-
理清libuv的“7队列”和Node“6队列”的关系
-
Node和浏览器环境下setTimeout的最小延迟时间
-
setTimeout和setImmediate的执行顺序详解
-
Node相关组成结构中涉及的数据结构
一.开门见山:Node和浏览器的异步执行顺序问题
>> Node端的异步执行顺序
同步代码 > process.nextTick > Promise.then中的函数 > setTimeOut(0) 或 setImmediate
-
「备注1」 Promise中的函数,无论是resolve前的还是后的,都属于“同步代码”的范围,并不是“异步代码”
-
「备注2」 setTimeOut(0) 或 setImmediate的执行顺序取决于具体情况,并没有确定的先后区分
>> Node端异步逻辑顺序实验论证
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 ('同步代码');
输出
console.log ('我是同步代码'); new Promise (function (resolve, reject) { console.log ('resolve前'); resolve (); console.log ('resolve后'); }).then (function () {}); console.log ('我是同步代码');
-
《Node.js官方文档:事件循环,定时器和 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'); });
>> 参考资料
-
《MDN: window.setImmediate》
二.两种环境下的宏任务和微任务阵营(macrotask && microtask)
我们上面讲述了不同的程序,它们的异步执行顺序的区别,其中我们发现,有的异步API执行快,而有的异步API执行慢,实际上,它们作为异步任务,被分成了宏任务和微任务两大阵营,同时整体表现出微任务执行快于宏任务的现象
>> 浏览器端的宏任务和微任务
-
宏任务(macrotasks):setTimeout, setInterval, I/O,setImmediate(如果存在),requestAnimationFrame(存在争议)
-
微任务 (microtasks) : process.nextTick, Promises,MutationObserver
>> 备注解释
-
备注1:MutationObserver是HTML5新增的用来检测DOM变化的,参考资料
-
备注2: 部分资料认为,requestAnimationFrame也属于宏任务,理由是:requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行,参考资料
>> Node端的宏任务和微任务
-
微任务:process.nextTick,promise.then
-
宏任务:setTimeout, setInterval,setImmediate
三.Node和浏览器的事件循环模型在实现层面的区别
浏览器的事件循环是在 HTML5 中定义的规范,而 Node 中则是由 libuv 库实现,这是它们在实现上的根本差别。也就是说,很多时候,他们的行为看起来很像,但event loop的内在实现却存在差别。
>> 浏览器的event loop
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的event loop
-
libuv官方文档:http://libuv.org/
-
libuv简单介绍:https://www.oschina.net/p/libuv
四.Node和浏览器的事件循环的任务队列
>> 参考资料
-
《Timers, Immediates and Process.nextTick— NodeJS Event Loop Part 2》
>> Node的任务队列
Node的任务队列总共6个:包括4个主队列(main queue)和两个中间队列(intermediate queue)
-
四个主队列由libuv提供
-
两个中间队列由Node.js实现
>> 6个队列具体内容
-
主队列(main queue):包括计时器队列,IO事件队列,即时队列,关闭事件处理程序队列
-
中间队列(intermediate queue):包括(1)Next Ticks队列和(2)其他微任务队列
>> 四个主队列
>> 两个中间队列
>> 主队列和中间队列的关系
>> 浏览器的任务队列
-
宏任务队列(macro task)
-
微任务队列。(micro task)
-
每次从宏任务队列中取一个宏任务执行, 完成后, 把微任务队列中的所有微任务,一次性处理完
-
不断重复上述过程
五.Node和浏览器的事件循环模型在表现层面的差异
-
在Node11.0.0以前的版本,Node和浏览器的异步流程存在一些细节上的差异,
-
但在Node11.0.0以后,这一差异被抹去了,因为Node主动修改了实现以和浏览器保持一致
吐槽:听话的Node.js
-
在浏览器和Node11以后,每执行完一个timer类回调,例如setTimeout,setImmediate 之后,都会把微任务给执行掉(promise等)。
-
原来Node10和以前: 当一个任务队列(例如timer queue)里面的回调都批量执行完了,才去执行微任务
setTimeout (function () { console.log ('timeout1:宏任务'); new Promise (function (resolve, reject) { resolve (); }).then (() => { console.log ('promise:微任务'); }); }); setTimeout (function () { console.log ('timeout2:宏任务'); });
-
如果是11以后的Node和浏览器:执行完第一个setTimeout后,接下来轮到Promise这类微任务执行了,所以接下来应该是输出「promise:微任务」
-
如果是version11以前的Node,则执行完第一个setTimeout后,因为timer队列没处理完,所以接下来执行的是第二个setTimeout,输出的是「timeout2:宏任务」
>> 参考资料
- 《New Changes to the Timers and Microtasks in Node v11.0.0 ( and above)》
六.理清libuv的“七队列”和Node“四个主队列”的关系
>> 我们首先要明白的是三点
-
这里的七队列是libuv内部的概念
-
之前介绍的"Node六队列"和"四个主队列"是Node内部,但在libuv外部的实现和概念
-
这两者之间存在对应关系,虽然不是一一对应(下面会细讲对应关系)
>> libuv七队列图解
>> 七队列的具体作用
-
timers:执行满足条件的 setTimeout 、setInterval 回调;
-
pending callbacks: 检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
-
idle:仅仅供给Node系统内部使用
-
prepare:仅仅供给Node系统内部使用
-
poll:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和 setImmediate() 排定的之外),其余情况 node 将在此处阻塞。
-
check:执行 setImmediate 的回调;
-
close callbacks:关闭所有的 closing handles ,一些 onclose 事件;
>> libuv七队列和Node四个主队列的对应关系
>> 参考资料
-
《Timers, Immediates and Process.nextTick— NodeJS Event Loop Part 3》
七.Node和浏览器环境下setTimeout的最小延迟时间
>> 浏览器端的最小延迟时间
“HTML5 规范规定最小延迟时间不能小于 4ms,即 x 如果小于 4,会被当做 4 来处理。 不过不同浏览器的实现不一样,比如,Chrome 可以设置 1ms,IE11/Edge 是 4ms。”
-
《JavaScript定时器和执行机制解析》— — 腾讯Alloyteam前端团队
>> Node端的最小延迟时间
-
《Does Node.js enforce a minimum delay for setTimeout? - Stack Overflow》
>> 我觉得里面有一句话说的特别好
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
八.setTimeout(0 delay)和setImmediate的执行顺序详解
>> 总结来说
-
在主线程中直接调用setTimeOut(0,function) 和setImmediate不能确定其执行的先后顺序
-
但是如果在同一个IO循环中,例如在一个异步回调中调用这两个方法,setImmediate会首先被调用
>> 具体解释
第一.在主线程中运行以下脚本,我们不能确定timeout和immediate输出的先后顺序,结果受到进程性能的影响 (例子源于Node官方文档,链接在下面给出)
// timeout_vs_immediate.js setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
输出结果无法确定
// timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
immediate timeout
九.Node相关组成结构中涉及的数据结构
>> 介绍
-
setTimeout与setInterval: 调用这两个函数创建的定时器会被插入到定时器观察者内部的一个红黑树中,每次tick执行时候都会从红黑树中迭代取出定时器对象。
-
process.nextTick: 将回调函数放入到队列中,在下一轮Tick时取出执行,可以达到setTimeout(fn,0)的效果,由于不需要动用红黑树,效率更高时间复杂度为O(1)。相比较之下。(红黑树时间复杂度O(lg(n)) )
-
setImmediate:的回调函数保存在链表中,每次Tick只执行链表中的一个回调函数。
>> 本节参考资料
-
《深入浅出Node.js》作者:朴灵,阿里巴巴数据平台资深开发者,被尊为Node.js的布道者