一、什么是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.htmlconst 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.0000775mstimeout callback executed after 1s and 0.0023212mstimeout 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.jstimeoutimmediate$ node timeout_vs_immediate.jsimmediatetimeout
起因是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 completesomeAsyncOperation(() => { 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 endtimefs.readFile('text.txt', () => { endtime = Date.now() console.log('finish reading time: ', endtime - starttime)})let index = 0function handler () { if (index++ >= 1000) return console.log(`nextTick ${index}`) process.nextTick(handler) // console.log(`setImmediate ${index}`) // setImmediate(handler)}handler()
process.nextTick()的运行后果:
nextTick 1nextTick 2......nextTick 999nextTick 1000finish reading time: 170
setImmediate(),运行后果:
setImmediate 1setImmediate 2finish reading time: 80......setImmediate 999setImmediate 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 公布!