概述:
event loop(事件循环)是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
宏队列:
宏队列,macrotask,也叫tasks。一些异步任务的回调会依次进入macrotask queue,等待后续被调用,这些异步任务包括:
setTimeout
setInterval
setImmediate (Node独有)
requestAnimationFrame (浏览器独有)
I/O
UI rendering (浏览器独有)
微队列:
微队列,microtask,也叫jobs。另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:
process.nextTick (Node独有)
Promise
Object.observe
MutationObserver
(注:这里只针对浏览器和NodeJS)
浏览器的Event Loop:
1.在主线程执行同步代码,完了以后
2.把微队列的排名第一的任务揪出来放到主线程执行,完了以后把微队列的排名第一的任务揪出来放到主线程执行....一直干空
3.把宏队列的排名第一的任务揪出来放到主线程执行,完了以后把微队列的排名第一的任务揪出来放到主线程执行,完了以后把微队列的排名第一的任务揪出来放到主线程执行....一直干空
323232323232........
PS:注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
NodeJS中的Event Loop:
但是在浏览器中,可以认为只有一个宏队列,所有的macrotask都会被加到这一个宏队列中,但是在NodeJS中,不同的macrotask会被放置在不同的宏队列中。NodeJS的Event Loop中,执行宏队列的回调任务有6个阶段:
各个阶段执行的任务如下:
timers阶段:这个阶段执行setTimeout和setInterval预定的callback
I/O callback阶段:执行除了close事件的callbacks、被timers设定的callbacks、setImmediate()设定的callbacks这些之外的callbacks
idle, prepare阶段:仅node内部使用
poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里
check阶段:执行setImmediate()设定的callbacks
close callbacks阶段:执行socket.on('close', ....)这些callbacks
每个阶段都有一个「先入先出队列」,这个队列存有要执行的回调函数(译注:存的是函数地址)。不过每个阶段都有其特有的使命。一般来说,当 event loop 达到某个阶段时,会在这个阶段进行一些特殊的操作,然后执行这个阶段的队列里的所有回调。
什么时候停止执行这些回调呢?下列两种情况之一会停止:
1.队列的操作全被执行完了
2.执行的回调数目到达指定的最大值
然后,event loop 进入下一个阶段,然后再下一个阶段。
一方面,上面这些操作都有可能添加计时器;另一方面,操作系统会向 poll 队列中添加新的事件,当 poll 队列中的事件被处理时可能会有新的 poll 事件进入 poll 队列。结果,耗时较长的回调函数可以让 event loop 在 poll 阶段停留很久,久到错过了计时器的触发时机。你可以在下文的 timers 章节和 poll 章节详细了解这其中的细节。
timers 阶段
计时器实际上是在指定多久以后可以执行某个回调函数,而不是指定某个函数的确切执行时间。当指定的时间达到后,计时器的回调函数会尽早被执行。如果操作系统很忙,或者 Node.js 正在执行一个耗时的函数,那么计时器的回调函数就会被推迟执行。
注意,从原理上来说,poll 阶段能控制计时器的回调函数什么时候被执行。
举例来说,你设置了一个计时器在 100 毫秒后执行,然后你的脚本用了 95 毫秒来异步读取了一个文件当 event loop 进入 poll 阶段,发现 poll 队列为空(因为文件还没读完),event loop 检查了一下最近的计时器,大概还有 100 毫秒时间,于是 event loop 决定这段时间就停在 poll 阶段。在 poll 阶段停了 95 毫秒之后,fs.readFile 操作完成,一个耗时 10 毫秒的回调函数被系统放入 poll 队列,于是 event loop 执行了这个回调函数。执行完毕后,poll 队列为空,于是 event loop 去看了一眼最近的计时器(译注:event loop 发现卧槽,已经超时 95 + 10 - 100 = 5 毫秒了),于是经由 check 阶段、close callbacks 阶段绕回到 timers 阶段,执行 timers 队列里的那个回调函数。这个例子中,100 毫秒的计时器实际上是在 105 毫秒后才执行的。
PS:注意:为了防止 poll 阶段占用了 event loop 的所有时间,libuv(Node.js 用来实现 event loop 和所有异步行为的 C 语言写成的库)对 poll 阶段的最长停留时间做出了限制,具体时间因操作系统而异。
I/O callbacks 阶段
这个阶段会执行一些系统操作的回调函数,比如 TCP 报错,如果一个 TCP socket 开始连接时出现了 ECONNREFUSED 错误,一些 *nix 系统就会(向 Node.js)通知这个错误。这个通知就会被放入 I/O callbacks 队列。
poll 阶段(轮询阶段)
poll 阶段有两个功能:
1.如果发现计时器的时间到了,就绕回到 timers 阶段执行计时器的回调。
2.然后再,执行 poll 队列里的回调。
当 event loop 进入 poll 阶段,如果发现没有计时器,就会:
1.如果 poll 队列不是空的,event loop 就会依次执行队列里的回调函数,直到队列被清空或者到达 poll 阶段的时间上限。
2.如果 poll 队列是空的,就会:
1.如果有 setImmediate() 任务,event loop 就结束 poll 阶段去往 check 阶段。
2.如果没有 setImmediate() 任务,event loop 就会等待新的回调函数进入 poll 队列,并立即执行它。
一旦 poll 队列为空,event loop 就会检查计时器有没有到期,如果有计时器到期了,event loop 就会回到 timers 阶段执行计时器的回调。
check 阶段
这个阶段允许开发者在 poll 阶段结束后立即执行一些函数。如果 poll 阶段空闲了,同时存在 setImmediate() 任务,event loop 就会进入 check 阶段。
setImmediate() 实际上是一种特殊的计时器,有自己特有的阶段。它是通过 libuv 里一个能将回调安排在 poll 阶段之后执行的 API 实现的。
一般来说,当代码执行后,event loop 最终会达到 poll 阶段,等待新的连接、新的请求等。但是如果一个回调是由 setImmediate() 发出的,同时 poll 阶段空闲下来了,event loop就会结束 poll 阶段进入 check 阶段,不再等待新的 poll 事件。
close callbacks 阶段
如果一个 socket 或者 handle 被突然关闭(比如 socket.destroy()),那么就会有一个 close 事件进入这个阶段
NodeJS中微队列主要有2个:
1.Next Tick Queue:是放置process.nextTick(callback)的回调任务的
2.Other Micro Queue:放置其他microtask,比如Promise等
在浏览器中,也可以认为只有一个微队列,所有的microtask都会被加到这一个微队列中,但是在NodeJS中,不同的microtask会被放置在不同的微队列中。
NodeJS的Event Loop过程:
1.初始化EventLoop
2.执行脚本同步代码
2.执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
3.开始执行macrotask宏任务,共6个阶段,先从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,
4.回到第二步,然后进入宏队列的第二个阶段,去执行完所有任务,完了再回到第二步,然后第三阶段.......
setImmediate 和 setTimeout :
setImmediate 和 setTimeout 很相似,但是其回调函数的调用时机却不一样。
setImmediate() 的作用是在当前 poll 阶段结束后调用一个函数。
setTimeout() 的作用是在一段时间后调用一个函数。
这两者的回调的执行顺序取决于 setTimeout 和 setImmediate 被调用时的环境。
如果 setTimeout 和 setImmediate 都是在主模块(main module)中被调用的,那么回调的执行顺序取决于当前进程的性能,这个性能受其他应用程序进程的影响。
举个例子,HTML5规范规定,setTimeout最少4毫秒延时,如果刚开始加载脚本用了5毫秒,当时间循环开始,执行宏任务的时候,tirmer阶段发现setTimeout已经超时间了,那么立即执行它。然而check阶段是排在tirmmer后面的,所以setTimeout先执行,
如果3毫秒就加载完了脚本,进入tirmer后发现setTimeout没到时间,就往下走,停留在poll阶段发现setTimeout到时间了,赶紧回到tirmer去执行,但是check阶段是必经之路,于是setlmmediate是要先执行的。
但是,如果把上面代码放到 I/O 操作的回调里,setImmediate 的回调就总是优先于 setTimeout 的回调
setImmediate 和 setTimeout setImmediate 和 setTimeout setImmediate 和 setTimeout setImmediate 和 setTimeout setImmediate 和 setTimeout