一、什么是 Event Loop
Event Loop 指的是计算机系统的一种运行机制,在 JavaScript 中就是采纳 Event Loop 这种机制来解决单线程带来的问题。
1.1. 对于 JavaScript 为什么要设计成单线程?
这次要和 js 的用处无关,js 是作为浏览器的脚本语言,次要是实现用户与浏览器的交互,以及操作 dom;这决定了它只能是单线程,否则会带来很简单的同步问题。
举个例子:如果 js 被设计了多线程,如果有一个线程要批改一个 dom 元素,另一个线程要删除这个 dom 元素,此时浏览器就会一脸茫然,手足无措。所以,为了防止复杂性,从一诞生,JavaScript 就是单线程,这曾经成了这门语言的外围特色,未来也不会扭转。
要了解 Event Loop,首先要了解程序运行的模式,运行当前的程序叫做过程,个别状况下一个过程一次只能执行一个工作,如果有多个工作须要执行,有三种解决办法:
(1)排队。 因为一个过程一次只能执行一个工作,只好等后面的工作执行完了,再执行前面的工作。
(2)新建过程。 应用 fork 命令,为每个工作新建一个过程。
(3)新建线程。 因为过程太消耗资源,所以现在的程序往往容许一个过程蕴含多个线程,由线程去实现工作。
1.2. 过程和线程的概念
简略点说,过程是车间,线程是打工仔,车间能够包容多个打工仔,打工仔们共用车间内的资源,一个打工仔一次只能做一件事件。
详情不再开展,对于过程和线程的概念这里举荐阮一峰老师的文章,图文并茂,解释的十分生动有趣:
https://www.ruanyifeng.com/bl…
1.3. 同步和异步 && 阻塞和非阻塞
JavaScript 是单线程语言,要执行多个工作只能排队,如果后面的同步工作耗时过长,势必会阻塞前面工作的执行。因而 JavaScript 须要一种异步机制来让解决这些耗时的工作,并且在解决实现后进行告诉,Event Loop 就设计进去了。
在浏览器端 JavaScript 次要是利用了浏览器内核多线程实现异步的,浏览器是一个多过程的架构,以开源的 Chromium 为例,它有五个过程,咱们须要关怀的是渲染过程,渲染过程属于浏览器外围过程。
以上面这段代码为例,先输入 something,1s 后再输入 timeout,因为定时器是在浏览器的定时触发器线程执行的,console.log 在 js 主线程执行,主线程代码执行实现后闲暇了才会去读队列中已实现的工作
setTimeout(() => {console.log('timeout')
}, 1000)
console.log('something')
对于浏览器多过程架构参考文章:
https://juejin.cn/post/684490…
在 Node 端同样也是借助多过程架构来实现异步的,很多人说 node.js 是单线程的,这个说法比拟全面,node.js 单线程的起因是因为它接管的工作是单线程的(参考前面的 node.js 整体运行机制),然而 Node 自身是一个多过程架构。
对于 Node 多过程架构参考文章:
https://juejin.cn/post/699960…
以下这些概念不必记,遗记了过去查一下即可,但理解这些概念有助于帮忙咱们了解 Event Loop 的运行机制。
1、什么是阻塞和非阻塞?
阻塞和非阻塞是针对于过程在拜访数据时,依据 IO 操作的就绪状态而采取的不同形式,简略来说是一种读取或写入操作函数的实现形式,阻塞形式下读取或写入函数将始终期待。非阻塞形式下,读取和写入函数会立刻返回一个状态值。
2、什么是异步和同步?
同步和异步是针对应用程序和内核的交互而言的,同步是指用户过程触发 IO 操作并期待或轮询的查看 IO 操作是否就绪,异步是指用户过程触发 IO 操作当前便开始做本人的事件,当 IO 操作实现时会失去告诉,换句话说异步的特点就是告诉。
3、什么是 I / O 模型?
一般而言,IO 模型能够分为四种:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
- 同步阻塞 IO 是指用户过程在发动一个 IO 操作后必须期待 IO 操作实现,只有当真正实现了 IO 操作后用户过程能力运行。
- 同步非阻塞 IO 是指用户过程发动一个 IO 操作后立刻返回,程序也就能够做其余事件。然而用户过程须要不断的询问 IO 操作是否就绪,这就要求用户过程不停的去询问,从而引入不必要的 CPU 资源节约。
- 异步阻塞 IO 是指利用发动一个 IO 操作后不用期待内核 IO 操作的实现,内核实现 IO 操作后会告诉应用程序。这其实是同步和异步最要害的区别,同步必须期待或被动询问 IO 操作是否实现,那么为什么说是阻塞呢?因为此时是通过 select 零碎调用来实现的,而 select 函数自身的实现形式是阻塞的,采纳 select 函数的益处在于能够同时监听多个文件句柄,从而进步零碎的并发性。
- 异步非阻塞 IO 是指用户过程只须要发动一个 IO 操作后立刻返回,等 IO 操作真正实现后,利用零碎会失去 IO 操作实现的告诉,此时用户过程只须要对数据进行解决即可,不须要进行理论的 IO 读写操作,因为真正的 IO 读写操作曾经由内核实现。
再看一张图加深一下了解:
更具体的概念请参考如下文章进行学习:
https://www.jianshu.com/p/458…
1.4. 为什么要设计 Event Loop?
这个问题反映到咱们生存中也是一样的情理,你一天要干很多事件,有一些事件又很耗时然而它又没那么重要,你会不会想方法来进步本人的效率好让本人解放出来呢?这样你能力正当利用一天的工夫去做更重要的事件。于是人们创造了各种工具,洗衣机、电饭煲等等。你不必关怀它是怎么做的,什么时候做完,洗好衣服你去晒,做好饭你去吃,你只须要把工作交给它,解决好了它就会告诉你,你什么时候有空了再去处理结果就能够了。
这个例子中,你是一个人,不能同时做很多事件,反映到代码中你是单线程,你把耗时的工作交给这些工具,工具实现后会告诉你后果,你就解放出来了能够先去做更重要的事件,这就是 Event Loop 的作用。
联合以上的知识点,我想你对为什么要设计 Event Loop 曾经有了本人的了解。
阮一峰老师的这篇文章也很好的解释了这个问题:
http://www.ruanyifeng.com/blo…
二、node 的 Event Loop
请留神,本节所学习的 node 版本为 v10,在 v10 版本之后 Event Loop 的行为曾经与浏览器保持一致。
2.1. Event Loop 设计理念
以下援用摘自 node 中文网
事件循环是 Node.js 解决非阻塞 I/O 操作的机制——只管 JavaScript 是单线程解决的——当有可能的时候,它们会把操作转移到零碎内核中去。
既然目前大多数内核都是多线程的,它们可在后盾解决多种操作。当其中的一个操作实现的时候,内核告诉 Node.js 将适宜的回调函数增加到 轮询(poll)队列中期待机会执行。
链接:https://nodejs.org/zh-cn/docs…
简略来说 Event Loop 就是一种解决非阻塞 I / O 操作的机制,借助内核多线程的特点,在后盾解决各种各样的操作,解决实现后内核会告诉 Node.js 来进行解决。
就像你去餐厅点餐一样,你不必关怀你点的这份餐怎么做进去的,餐做好了就会在大厅叫号牌上显示对应的号码,而后揭示你取餐,你不须要站在那期待出餐这个漫长的过程。
在高性能的 I / O 设计中,有两个比拟驰名的模式 Reactor 和 Proactor 模式,其中 Reactor 模式用于同步 I /O,Proactor 用于异步 I / O 操作。
node 采纳了 Reactor 设计模式。那么什么是 Reactor 模式?
Reactor 模式是解决并发 I / O 常见的一种模式,用于同步 I /O,其中心思想是将所有要解决的 I / O 事件注册到一个核心 I / O 多路复用器上,同时主线程阻塞在多路复用器上,一旦有 I / O 事件到来或是准备就绪,多路复用器将返回并将相应 I / O 事件散发到对应的处理器中。
Reactor 是一种事件驱动机制,和一般函数调用不同的是应用程序不是被动的调用某个 API 来实现解决,恰恰相反的是 Reactor 逆置了事件处理流程,应用程序需提供相应的接口并注册到 Reactor 上,如果有相应的事件产生,Reactor 将被动调用应用程序注册的接口(回调函数)。
对于 Reactor 设计模式可学习如下文章:
https://www.jianshu.com/p/458…
2.2. libuv
咱们都晓得 node 可能运行在不同的平台上,因为在不同操作系统平台上反对所有类型的非阻塞 I / O 十分艰难和简单,就 须要有一个形象层来治理这些简单的,跨平台的货色,这个形象层就是——libuv。
Event Loop 就是由 libuv 提供的。
以下援用自 libuv 官网文档
libuv 是一个跨平台的反对库,最后是为 Node.js 编写的。它是围绕事件驱动的异步 I/O 模型设计的。
该库提供的不仅仅是针对不同 I / O 轮询机制的简略形象,“handles”和“streams”为套接字和其余实体提供了更高级形象;除此之外,还提供了跨平台文件 I / O 和线程性能。
对 libuv 感兴趣请参考链接进行学习:http://docs.libuv.org/en/v1.x…
2.3. node.js 的整体运行机制
上图中,用户输出 JavaScript 代码,由 V8 引擎进行解析,V8 调用 Node API 而后由 libuv 进行解决,libuv 提供 Event Loop 来解决各类工作,解决实现后将后果返回给 V8,V8 再将后果返回给用户。
node.js 应用了事件驱动模型,该模型蕴含一个 Event Demultiplexer 和一个 Event Queue,所有的 I / O 申请最终会生成一个 completion/failure 事件或其余触发器,事件会依据以下算法进行解决:
- Event Demultiplexer 接管 I / O 申请并将这些申请委托给适当的硬件
- 一旦解决了 I / O 申请(例如,文件中的数据可供读取、套接字中的数据可供读取等),Event Demultiplexer 会把相应的回调函数增加到一个队列外面。这些回调称为事件,增加事件的队列称为 Event Queue
- 解决 Event Queue 中的事件时,将依照事件增加的程序顺次执行,直到队列为空。
- 如果 Event Queue 中没有事件,或者 Event Demultiplexer 没有挂起的申请,程序将实现。否则,反复下面的步骤。
调度整个机制的程序称为事件循环(Event Loop)。
Event Demultiplexer
Event Demultiplexer 不是实在存在的一个部件,它仅仅是 Reactor Pattern 的一个形象。在实在环境中,不同的操作系统都会实现本人的 Event demultiplexer。如 linux 上 的 epoll、bsd 零碎(macos)上的 kqueue、solaris 中的 event ports、windows 中的 iocp 等。node.js 能够通过这些已实现的 Event demultiplexer 应用无阻塞、异步的 I /O。
Event Queue
- nodejs 中有多个队列,其中不同类型的事件在它们本人的队列中排队。
- 在解决完一个阶段之后,在转到下一个阶段之前,事件循环将解决两个两头队列,直到两头队列中没有残余的项为止。
2.4. 有多少种队列?两头队列是什么?
有 4 种类型的队列由本机 libuv 事件循环解决:
- timers 队列——已过期的 timer 回调(setTimeout、setInterval)
- I/ O 事件队列——已实现的 I / O 事件(readFile 等)
- Immediates 队列——setImmediate 的回调
- close 事件队列——close 事件的回调(socket 敞开等)
还有 2 个两头队列:(尽管这两个队列不属于 libuv,但它们是 node.js 的一部分)
- nextTick 队列——process.nextTick 的回调
- microtask 队列——如 promise
libuv 引擎 Event Loop 解决这几种队列的循环图如下:
Event Loop 启动后,首先解决 timers 队列,再到 I / O 事件队列,接着解决 immediate 队列,最初解决 close 事件队列,在解决完一个阶段行将要切换到下一个阶段之前,会先解决 nextTick 队列,清空 nextTick 队列之后再解决 microtask 队列,等到 microtask 队列清空后才会进入下一个阶段。
nextTick 队列比 microtask 队列优先级更高,意味着如果有 nextTick 队列,会在解决 microtask 队列之前就把 nextTick 队列都清空。
参考文章:https://zhuanlan.zhihu.com/p/…
2.5. Event Loop 运行机制
node.js 启动时会初始化事件循环(Event Loop)机制,每次循环都会蕴含如下 6 个阶段,每个阶段都有一个先进先出(FIFO)的用于执行回调的队列 ,通常事件循环 运行到某个阶段时,node.js 会先执行该阶段的操作,而后再去执行该阶段队列里的回调,直到队列里的内容耗尽,或者执行的回调数量达到最大(maximum number,最大值由以后机器性能决定)。每一个阶段实现后,事件循环就会查看这两个两头队列中是否有内容,如果有立马执行,直到这两个队列清空为止,等到它们清空,事件循环才会进入下一个阶段,如此往返循环,直到过程完结。
liubv 引擎 Event Loop 的 6 个阶段:
- timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调
- I/O callbacks(pending callbacks)阶段:已实现的、报错且未被解决的 I / O 的回调都会在这里被执行
- idle, prepare 阶段:仅 node 外部应用,只是表白闲暇、准备状态(第二阶段完结,poll 未触发之前)
- poll 阶段:期待任意一个新的 I / O 实现,执行 I / O 相干回调,适当的条件下 node 将阻塞在这里
- check 阶段:在轮询 I / O 之后执行一些预先工作,通常是执行 setImmediate() 的回调
- close callbacks 阶段:执行一些敞开的回调函数,如执行 socket 的 close 事件回调
在 node.js 里,任何异步办法(除 timer,close,setImmediate 之外)实现时,都会将其 callback 加到 poll queue 里,并立刻执行。
通过下面的常识能够总结出:一个阶段执行结束进入下一个阶段之前,Event Loop 会先清空 microtask 队列的工作(如果有 nextTick 队列,则先清空 nextTick 队列而后再清空 microtask 队列),等到 microtask 队列清空后再进入下一个阶段,如下图所示:
咱们重点看 timers、poll、check 这 3 个阶段就好,因为日常开发中的绝大部分异步工作都是在这 3 个阶段解决的。
2.5.1. timers 阶段
timers 是事件循环的第一个阶段,当咱们应用 setTimeout 或者 setInterval 时,node 会增加一个 timer 到 timers 堆,当事件循环进入到 timers 阶段时,node 会查看 timers 堆中有无过期的 timer,如果有,则顺次执行过期 timer 的回调函数。
对于 timers 堆学习参考:https://blog.csdn.net/tinnfu/…
须要留神的是,node 不能保障到了过期工夫就立刻执行回调函数,因为它在 执行回调前必须先查看 timer 是否过期 ,查看的过程是须要 耗费工夫 的,这个工夫的长短 取决于零碎性能 ,性能越好执行速度越快,另外一点是, 如果以后 Event Loop 中还有别的过程在执行,也会影响 timer 回调的执行。这与浏览器的 Event Loop 机制是相似的,浏览器环境中如果定时器在一个十分耗时的 for 循环之后运行,尽管工夫已过期,依然要等到 for 循环计算实现才会执行定时器的回调。
在达到过期工夫之间的工夫称为有效期,定时器可能保障的就是至多在给定的有效期内不会触发定时器回调。
以下援用自 node 中文网:
计时器指定 能够执行所提供回调 的 阈值,而不是用户心愿其执行的确切工夫。在指定的一段时间距离后,计时器回调将被尽可能早地运行。然而,操作系统调度或其它正在运行的回调可能会提早它们。
留神:轮询(poll)阶段 管制何时定时器执行。
也就是说:poll 阶段管制 timer 什么时候执行,而执行的具体位置在 timers 阶段
示例:创立一个延时 1s 的 setTimeout,记录执行回调破费的工夫
// 获取纳秒级计时精度 -node 才有
// 参考文档:https://www.cnblogs.com/boychenney/p/12195632.html
const start = process.hrtime();
setTimeout(() => {const end = process.hrtime(start);
console.log(`timeout callback executed after ${end[0]}s and ${end[1]/Math.pow(10,9)}ms`);
}, 1000);
屡次运行程序,你会发现它每次打印的后果都不同,执行回调的距离都大于 1s
timeout callback executed after 1s and 0.0000775ms
timeout callback executed after 1s and 0.0023212ms
timeout callback executed after 1s and 0.000102ms
......
在 node 中,setTimeout 和 setImmediate 在一起应用时也会产生不同的状况,例如:
setTimeout(function timeout () {console.log('timeout');
}, 0);
setImmediate(function immediate () {console.log('immediate');
});
下面的代码第一眼看上去必定总是先打印 timeout,再打印 immediate,因为 setImmediate 的回调在 check 队列中,依照步骤应该是先查看过期 timer,而后再到 check 队列中执行 setImmediate 的回调才对。
理论后果并不是这样,如下所示,屡次运行后会失去不同的输入后果
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
起因是 setTimeout(fn, 0)无奈保障 timer 回调在 0 秒后立刻被调用,通过后面的学习咱们晓得当 Event Loop 启动时会先查看 timer 是否过期。如果它查看的过程耗时比拟长,它可能不会立刻看到过期的 timer,而后就略过了 timer 阶段走走走走到了 check 阶段,看到 check 队列有一个事件,于是执行输入 immediate,之后在下一个工夫循环中执行 setTimeout 的回调。
然而,当二者在异步 I /O callback 外部调用时,总是先执行 setImmediate,再执行 setTimeout
var fs = require('fs')
fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout')
}, 0)
setImmediate(() => {console.log('immediate')
})
})
// 屡次执行后果雷同
// immediate
// timeout
了解了 Event Loop 的各阶段程序这个例子很好了解:
因为 fs.readFile callback 执行完后,程序设定了 timer 和 setImmediate,因而 poll 阶段不会被阻塞进而进入 check 阶段先执行 setImmediate,后进入 timer 阶段执行 setTimeout。(前面会具体给出运行步骤)
二者十分类似,然而二者区别取决于他们什么时候被调用
- setImmediate 设计在 poll 阶段实现时执行,即 check 阶段;
- setTimeout 设计在 poll 阶段为闲暇时,且设定工夫达到后执行(在 poll 阶段阻塞时会查看有无过期 timer,有则回到 timers 阶段执行 timer 的回调);
其二者的调用程序取决于以后 Event Loop 的上下文,如果他们在异步 i/o callback 之外调用,其执行先后顺序是不确定的,执行的程序不确定,就是因为每一次 loop,最开始和完结时都查看 timer 的缘故。
2.5.2. poll 阶段
poll 阶段次要有 2 个性能:
- 解决 poll 队列的事件
- 计算应该阻塞和轮询 I / O 的工夫(当有新的 I / O 实现,I/O callback 退出 poll queue,而后执行 I /O callback;当有已超时的 timer,进入 timers 阶段执行它的回调函数)
如果 event loop 进入了 poll 阶段,且代码未设定 timer,将会产生上面状况:
- 如果 poll queue 不为空,event loop 将同步的执行 queue 里的 callback, 直至 queue 为空,或执行的 callback 达到零碎下限;
-
如果 poll queue 为空,将会产生上面状况:
- 如果代码曾经被 setImmediate()设定了 callback, event loop 将完结 poll 阶段进入 check 阶段,并执行 check 阶段的 queue (check 阶段的 queue 是 setImmediate 设定的)
- 如果代码没有设定 setImmediate(callback),event loop 将阻塞在该阶段期待 callbacks 退出 poll queue,而后立刻执行;
如果 event loop 进入了 poll 阶段,且代码设定了 timer:
- 如果 poll queue 进入空状态时(即 poll 阶段为闲暇状态),event loop 将查看 timers,如果有 1 个或多个 timers 工夫工夫曾经达到,event loop 将按循环程序进入 timers 阶段,并执行 timer queue。
通过下图再来加深一下了解
- Event Loop 进入 poll 阶段,进入 poll 阶段后查看 poll 阶段队列中是不是空的或者 callbacks 数量到了下限
- 如果不是空的 callbacks 也没有到下限,就执行 poll 队列外面的 callback,循环这个过程直到 poll 队列空了或者到了限度
- 这个时候看一下 setImmedidate 有没有设置 callback,如果有就进入 check 阶段
- 如果没有设置就会有一个期待状态,会期待 callback 退出 poll 队列外面,此时如果有新的 callback,就会再次进入 poll 队列去查看,而后循环下面的步骤
- 在期待 callback 退出 poll 队列闲暇的时候,会去查看定时器有没有到工夫,如果定时器到工夫了又有对应的 callback,它就会进入 timers 定时器阶段去执行 timer queue 中的 callback
- 如果定时器没有到工夫,就会持续期待
node 官网提供的一个示例:假如您调度了一个在 100 毫秒后超时的定时器,而后您的脚本开始异步读取会消耗 95 毫秒的文件
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {// do nothing}
});
代码中创立了一个 someAsyncOperation 异步读文件的办法,而后创立了一个常量 timeoutScheduled 取以后工夫,而后有一个 setTimeout,外面计算延时多少毫秒之后打印一句话,最初一段是调用 someAsyncOperation 这个异步办法,读完文件后执行 callback,callback 外面执行一个空的 while 循环,咱们能够了解为睡眠 10 毫秒。
当事件循环进入 轮询 阶段时,它有一个空队列(此时 fs.readFile() 尚未实现),因而它将期待剩下的毫秒数,直到达到最快的一个计时器阈值为止。当它期待 95 毫秒过后时,fs.readFile() 实现读取文件,它的那个须要 10 毫秒能力实现的回调,将被增加到 轮询 队列中并执行。当回调实现时,队列中不再有回调,因而事件循环机制将查看最快达到阈值的计时器,而后将回到 计时器 阶段,以执行定时器的回调。在本示例中,您将看到调度计时器到它的回调被执行之间的总提早将为 105 毫秒。
2.5.3. check 阶段
这个阶段容许在 poll 阶段完结后立刻执行回调,如果 poll 阶段闲暇并且有被 setImmediate 设置回调,那么事件循环间接跳到 check 阶段执行而不是阻塞在 poll 阶段期待回调被退出。
setImmediate 实际上是一个非凡的 timer,跑在事件循环中的一个独立的阶段。它应用 libuv 的 API 来设定在 poll 阶段完结后立刻执行回调。setImmediate 的回调会被退出 check 队列中,从 event loop 的阶段图能够晓得,check 阶段的执行程序在 poll 阶段之后。
2.5.4. 小结
- event loop 的每个阶段都有一个该阶段对应的队列和一个 microtask 队列
- 当 event loop 达到某个阶段时,将执行该阶段的工作队列(先执行阶段队列,再执行 microtask 队列),直到队列清空或执行的回调达到零碎下限后,才会转入下一个阶段
- 当所有阶段被程序执行一次后,称 event loop 实现了一个 tick
再来看一段代码示例:(假如要读取的文件须要 100ms)
const fs = require('fs')
fs.readFile('test.txt', () => {console.log('readFile')
setTimeout(() => {console.log('timeout')
}, 0)
setImmediate(() => {console.log('immediate')
})
})
// 执行后果:// readFile
// immediate
// timeout
以上代码执行程序为:
程序启动时,Event Loop 初始化:
- 进入 timers 阶段,查看有无过期 timer,没有(如果有则执行 timer queue 中的 callback),进入下一个阶段
- 进入 I / O 阶段,无异步 I / O 实现(可疏忽)
- 进入 idls 阶段,啥也没有,进入下一个阶段(可疏忽)
-
进入 poll 阶段(没有设置 timer),当初 poll queue 是空的(此时 fs.readFile 尚未实现),因而它进入期待状态,直到有工作退出队列中,当它等到 100ms 时 fs.readFile 实现,将 callback 退出 poll queue,并执行 callback,callback 执行打印 readFile 并设置了一个 setTimeout 和一个 setImmediate,而后 callback 执行实现,poll queue 清空。(如果没有设置 setImmediate 的状况下,当 callback 实现时,Event Loop 将查看有没有到工夫的 timer,有的话会回到 timers 阶段来执行 timer 的回调)
- readFile 回调执行打印 readFile,而后设置了 timer,将 timer 的回调放入 timer queue,将 setImmediate 的回调放入 check queue,依据规定,Event Loop 进入 poll 阶段前如果未设置 timer 并且 poll 队列为空会有两种状况,其中一种是如果代码曾经被 setImmediate 设置了 callback,Event Loop 将完结 poll 阶段进入 check 阶段,并执行 check queue 中的事件,很显著本例中应用 setImmediate 设置了 callback
- 因而 poll 阶段不会被阻塞而是进入 check 阶段,执行 setImmediate 的回调函数,打印 immediate,check queue 清空,进入下一个阶段
- 进入 close 阶段,没有工作,一次 Tick 实现,进入下一次 Tick
- 进入 timers 阶段,查看 timer queue 有没有过期的 timer,有,执行 readFile 回调中设置的 setTimeout 回调,打印 timeout,进入下一个阶段
- ······
三、浏览器端和 node 端 Event Loop 执行过程比照
通过以下代码来具体察看一下浏览器端和 node 端的执行过程:
setTimeout(()=>{console.log('timer1')
Promise.resolve().then(function() {console.log('promise1')
})
}, 0)
setTimeout(()=>{console.log('timer2')
Promise.resolve().then(function() {console.log('promise2')
})
}, 0)
3.1. 浏览器的 Event Loop 流程解析
浏览器端运行后果:timer1 => promise1 => timer2 => promise2
浏览器 Event Loop 执行动画示意:
浏览器端的执行过程是:
- 主程序 main()入栈执行,遇到第一个 timer,将 timer 的回调存入宏工作队列(macrotask),持续往下执行,遇到第二个 timer,将回调存入宏工作队列,main()执行实现退栈
- Event Loop 开始查看宏工作队列,执行第一个 timer 的回调函数,打印 timer1,并将 promise.then()的回调存入微工作队列,timer1 的回调执行实现退栈,而后执行微工作队列中的所有工作,打印 promise1,再查看有没有宏工作,有,执行,打印 timer2,并将 promise.then()的回调函数存入微工作,timer2 的回调函数执行实现退栈,查看微工作队列并执行,打印 promise2
须要留神的是:浏览器 Event Loop 的宏工作是一个一个执行的,微工作是一队一队执行的,执行完每个宏工作之后都会查看微工作队列,直到将所有微工作都清空后才会持续下一个宏工作,每次将一队微工作执行实现后就会执行渲染操作更新界面。
以下是浏览器端 Event Loop 的执行流程图:
3.2. node 端 Event Loop 的流程解析
node 端运行后果:timer1 => timer2 => promise1 => promise2
node 的 Event Loop 执行动画示意:
node 端 Event Loop 执行过程分两种状况:
1、查看过期 timer 耗费的工夫小于阈值:
- 主程序 main()入栈执行,将 2 个 timer 放入 timer 队列
- Event Loop 初始化,进入 timers 阶段,查看有无过期 timer,有,执行 timer queue 中的 callback,打印 timer1、timer2,并且别离将两个 promise 放入 microtask queue,执行实现后 timer queue 清空,进入下一步,查看 nextTick queue,没有,查看 microtask queue,有,执行 microtask queue,打印 promise1、promise2,而后 microtask queue 清空,进入下一步
- ······
2、查看过期 timer 耗费的工夫大于阈值
- 主程序 main()开始执行,将 2 个 timer 放入 timer queue
- Event Loop 初始化,进入 timers 阶段,查看有无过期 timer,无,进入下一个阶段
- 进入 I / O 阶段和 idle 阶段(疏忽)
- 进入 poll 阶段,查看 poll queue,空,期待工作退出的过程中查看有无过期 timer,有(假如此时 timer 过期,如果没过期 poll 阶段则会持续阻塞期待新工作,期待时会查看有无到期 timer),进入 timers 阶段,执行 timer queue 中的 callback,打印 timer1、timer2,并且别离将两个 promise 放入 microtask queue,执行实现后 timer queue 清空,进入下一步,查看 nextTick queue,没有,查看 microtask queue,有,执行 microtask queue,打印 promise1、promise2,而后 microtask queue 清空,进入下一个阶段
- ······
四、process.nextTick()
4.1. process.nextTick()介绍
官网是这么形容 process.nextTick()的
链接:https://nodejs.org/zh-cn/docs…
process.nextTick 的回调函数会被增加到 nextTickQueue,nextTickQueue 比其余 microtaskQueue 具备更高的优先级。只管它们都在事件循环的两个阶段之间被解决。这意味着 nextTickQueue 在开始解决 microtaskQueue 之前就曾经被清空。
nextTickQueue 的优先级高于 promises,仅仅实用于 promises 是通过 v8 解析产生的。如果你用了 q 或者 bluebird,你会察看到齐全不同的后果,因为他们先于 promises 执行,且具备不同的语义。
q 和 bluebird 在解决 promise 的形式上是不一样的。
对于 libuv 引擎 Event Loop 如何解决 promise(蕴含原生 Promise、Q promise 和 BlueBird Promise)和 nextTick 请参考如下文章:
https://zhuanlan.zhihu.com/p/…
process.nextTick()不在 event loop 的任何阶段执行,而是在各个阶段切换的两头执行,即从一个阶段切换到下个阶段前执行。
示例:
var fs = require('fs');
fs.readFile(__filename, () => {setTimeout(() => {console.log('setTimeout');
}, 0);
setImmediate(() => {console.log('setImmediate');
process.nextTick(()=>{console.log('nextTick3');
})
});
process.nextTick(()=>{console.log('nextTick1');
})
process.nextTick(()=>{console.log('nextTick2');
})
});
// 运行后果
// nextTick1
// nextTick2
// setImmediate
// nextTick3
// setTimeout
以上代码执行程序为:
- 从 poll —> check 阶段,先执行 process.nextTick,打印 nextTick1、nextTick2
- 而后进入 check 阶段,打印 setImmediate
- 执行完 setImmediate 后,出 check,进入 close 阶段前,执行 process.nextTick,打印 nextTick3,一次 Tick 实现,进入下一次 Tick
- 进入 timer 执行 setTimeout,打印 setTimeout
- ······
4.2. process.nextTick() VS setImmediate()
来自官网文档有意思的一句话,从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相同,命名是历史起因也很难再变。
In essence, the names should be swapped. process.nextTick() fires more immediately than setImmediate()
process.nextTick() 会在各个事件阶段之间执行,一旦执行,要直到 nextTick 队列被清空,才会进入到下一个事件阶段,所以如果递归调用 process.nextTick(),会导致呈现 I /O starving(饥饿)的问题,比方上面例子的 readFile 曾经实现,但它的回调始终无奈执行:
const fs = require('fs')
const starttime = Date.now()
let endtime
fs.readFile('text.txt', () => {endtime = Date.now()
console.log('finish reading time:', endtime - starttime)
})
let index = 0
function handler () {if (index++ >= 1000) return
console.log(`nextTick ${index}`)
process.nextTick(handler)
// console.log(`setImmediate ${index}`)
// setImmediate(handler)
}
handler()
process.nextTick()的运行后果:
nextTick 1
nextTick 2
......
nextTick 999
nextTick 1000
finish reading time: 170
setImmediate(),运行后果:
setImmediate 1
setImmediate 2
finish reading time: 80
......
setImmediate 999
setImmediate 1000
这是因为嵌套调用的 setImmediate() 回调,被排到了下一次 Event Loop 才执行,所以不会呈现阻塞。
process.nextTick()是 node 晚期版本无 setImmediate 时的产物,node 作者举荐咱们尽量应用 setImmediate。
4.3. 为什么要应用 process.nextTick()?
以下是来自官网的答复:
链接:https://nodejs.org/zh-cn/docs…
我的了解是:
- process.nextTick()是一个弱小的异步 API,当咱们须要控制代码程序,保障同步和异步如期执行时,能够思考应用它。
举个例子,比方咱们在执行一个十分耗时的计算函数时,如果同步执行函数,因为单线程的缘故势必会阻塞前面代码的执行,所以咱们能够将函数交给 process.nextTick(),相当于放开计算函数的使用权,通过 process.nextTick()办法将该函数的使用权交给计算机系统,就像在说:“我把使用权交给你,你有空了就帮我计算一下,计算完了通过 callback 通知我”。
4.4. 小结
- node.js 的事件循环分为 6 个阶段
-
浏览器和 Node 环境下,microtask 工作队列的执行机会不同
- Node.js 中,microtask 在事件循环的各个阶段之间执行
- 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
- 递归的调用 process.nextTick()会导致 I /O starving,官网举荐应用 setImmediate()
五、参考
什么是 Event Loop?(前置常识,帮忙咱们理解 Event Loop)
JavaScript 运行机制详解:再谈 Event Loop(前置常识,通过 JavaScript 的运行机制帮忙咱们了解 Event Loop)
[[译]官网图解:Chrome 快是有起因的,古代浏览器的多过程架构!](https://juejin.cn/post/684490…)
Node.js Event Loop 的了解 Timers,process.nextTick()
(评论区异样精彩,有源码解析,肯定要看,肯定要看,肯定要看,重要的事说三遍!)
[[翻译]Node 事件循环系列——1、事件循环总览](https://zhuanlan.zhihu.com/p/…)(全系列文章都十分值得学习)
深刻了解 js 事件循环机制(Node.js 篇)
深刻了解 js 事件循环机制(浏览器篇)
nodejs 是代表 Reactor 还是 Proactor 设计模式?
Node.js 事件循环,定时器和 process.nextTick()
弱小的异步专家 process.nextTick()
本文由博客一文多发平台 OpenWrite 公布!