前言
在上一篇中我们详细的讲述了Event loop的基础概念及浏览器事件环的原理。本文中将对比浏览器事件环来说明下node的事件环原理。 如果你没有阅读过上一篇关于Event loop基础的说明,建议移步到上一篇文章中先阅读一下。
node事件环与浏览器事件环的区别
- node相比浏览器多了一个微任务process.next,且process.next执行的优先级比promise.then快
- 浏览器中异步任务只有微任务和宏任务两种优先级别,在node中宏任务也是有优先级别的,它的优先级由libuv内部的事件环机制决定。具体如下图: 通过上图我们可以知道node中的异步任务大致分为几个执行阶段:
- timers :是执行setTimeout、setInterval回调的,当定时器到时间后,会被放入放入timers对应的宏任务队列中;
- I/O callbacks阶段:执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
- idle, prepare 阶段: 仅node内部使用;
- poll 阶段: 这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。 这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
- check 阶段: 执行setImmediate() 设定的callbacks;
- close callbacks 阶段: 比如socket.on(‘close’,callback)的callback会在这个阶段执行.
上述的六个循环阶段,每一个循环阶段都对应这一个宏任务队列,用于存放当前阶段可执行的任务。
3.检查微任务队列的时机不一样,node是切换事件环阶段的时,即切换到下一个循环阶段时。浏览器是主线程执行栈被清空时。 4.在node中宏任务多了一个setImmediate,它属于六个轮询中的check阶段poll阶段
poll阶段是衔接整个event loop各个阶段比较重要的阶段。
在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。 poll 阶段有两个主要的功能:- 处理poll队列(poll quenue)的事件(callback);
- 执行timers的callback,当到达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.
代码示例
-
nextTick比then快示例:
console.log('1');setTimeout(function () { console.log('2'); process.nextTick(function () { console.log('3'); }) new Promise(function (resolve) { console.log('4'); resolve(); }).then(function () { console.log('5') })})new Promise(function (resolve) { console.log('7'); resolve();}).then(function () { console.log('8')})process.nextTick(function () { console.log('6');})setTimeout(function () { console.log('9'); process.nextTick(function () { console.log('10'); })})复制代码
执行结果:1、7、6、8、2、4、9、3、10、5
执行的原理:- 开始执行,首先将同步任务执行完成,输出1、7,同时将nextTick及then(输出8的then)放入微任务队列中、将setTimeout如果计时完成,则放入libuv的times阶段的队列
- 检查微任务队列,优先输出process.nextTick的6,然后输出then的8
- 进入libuv的times阶段,执行对应的宏任务队列,输出2,将nextTick(3)放入微任务队列,输出promise的4,将then(5)放入微任务队列,执行下一个setTimeout输出9,将nextTick(10)放入微任务队列
- 进入libuv的times阶段执行完成,检查为任务队列,优先输出3、10,然后输出5
-
setTimeout和setImmediate不一定谁先谁后示例:
setTimeout(function () { console.log('setTimeout’); },0); setImmediate(function () { console.log('setImmediate’); });复制代码
输出结果:不一定谁前谁后
执行的原理:- node执行需要准备的时间,如果准备的时间比定时器的时间长,当准备完成时,setTimeout已经被放入了执行timers(计时器)执行队列中。
- 当node的执行环境准备完成后,按照libuv的执行机制的顺序进行检查,先看timers,发现有任务则执行,没有则向下查找其他的阶段的对应队列,走到check(检查阶段的时候),setImmediate的回调事件已经在队列中了,则输出setImmediate,执行之后继续检查下一阶段的任务
- 如果在2中先检查到timers任务队列中有任务,则先输出setTimeout,如果第一次没检测到,则先输出的就是setImmediate
小结:谁先输出是由node的准备时间和定时器设置的时间谁快来决定的,定时器快则先输出setTimeout,否则先输出setImmediate
-
poll的下一个阶段是check的示例:
let fs = require('fs');fs.readFile('./1.txt', function () { setTimeout(() => { console.log('setTimeout') }, 0); setImmediate(() => { console.log('setImmediate') });});复制代码
执行结果:setImmediate、setTimeout
原理分析: 因为fs的回调函数执行的阶段属于poll阶段,poll执行完,接下来的阶段就是check阶段,所以一定是setImmediate先输出