我们知道,Node是基于Chrome V8 引擎的,也就是说它也有js引擎的事件循环也就是 Event Loop 机制。但是Node是运行在服务端的,是有区别于浏览器端的。比如比浏览器的异步API多了 setImmediate, process.nextTick 等。那么事件循环的机制也肯定是不一样的。下面我们就来看下 Node 的事件循环是怎么样的一个运作。
1. Node 的事件循环可以简单地看成是由一个 线程(即js主线程) 跟 好几个队列(比如nextTickQueue, microTaskQueue 等)组成的。主线程在不停地从队列中获取任务并执行,类似于在 while(true) {} 里面一直不断地循环
2. 事件循环是在主线程上面完成的
3. 事件循环包括同步任务跟异步任务,同步任务的执行优先级大于异步任务,也就是同步任务永远是在异步任务之前执行的:
1 console.log(‘我是同步任务,第一个输出‘) 2 process.nextTick(() => { 3 console.log(‘我是异步任务,第三个输出‘) 4 }) 5 setTimeout(() => { 6 console.log(‘我是异步任务,第四个输出‘) 7 }) 8 setImmediate(() => { 9 console.log(‘我是异步任务,第五个输出‘) 10 }) 11 console.log(‘我是同步任务,第二个输出‘) 12 // 输出结果 13 // 我是同步任务,第一个输出 14 // 我是同步任务,第二个输出 15 // 我是异步任务,第三个输出 16 // 我是异步任务,第四个输出 17 // 我是异步任务,第五个输出
4. 为了方便理解,这里暂且把异步任务分为我们常说的微任务(microTask)跟宏任务(macroTask)。microTask包含nextTickQueue跟microTaskQueue(为了方便理解,后面的microTask都是指nextTickQueue跟microTaskQueue),macroTask包含 setTimeout/setInterval/setImmediate/IO callback/http callback 等。其中 microTask 会在当前循环中执行,而 macroTask则是当当前循环里面的同步任务跟microTask里面的任务执行完之后才会执行。而在执行macroTask的时候,又会优先执行里面的同步任务,然后再检查 microTask 里面有没有任务,有的话执行完再继续下一个循环,没有的话则直接执行下一个宏任务。这样一轮一轮无限循环知道服务退出。这就是事件循环。
5. 在当前循环中,js线程的执行顺序是这样的:同步任务 > nextTickQueue > microTaskQueue ,也就是先执行同步任务,同步任务执行完之后检查 nextTickQueue 队列里面有没有任务,有的话执行 nextTickQueue,直到 nextTickQueue 里面的任务执行完之后,再判断 microTaskQueue 队列有没有任务,有的话执行,没有的话继续下一循环。需要注意的是当执行 microTaskQueue 任务时,如果又出现 process.next() 跟 Promise.then(), 也会先执行完 microTaskQueue,直到 microTaskQueue 为空,再判断 nextTickQueue :
1 console.log(‘1‘) 2 process.nextTick(() => { 3 console.log(‘tick 1‘) 4 process.nextTick(() => { 5 console.log(‘tick 2‘) 6 }) 7 }) 8 9 Promise.resolve().then(() => { 10 console.log(‘Promise.then 1‘) 11 process.nextTick(() => { 12 console.log(‘tick 3‘) 13 }) 14 Promise.resolve().then(() => { 15 console.log(‘Promise.then 2‘) 16 }) 17 }) 18 // 输出结果: 19 // 1 20 // tick 1 21 // tick 2 22 // Promise.then 1 23 // Promise.then 2 24 // tick 3
6. 当执行完当前宏任务之后,又会去队列里面判断是否有宏任务,有的话拿出来继续执行。
7. 事件循环的架构
8. 由上图我们可以看到,事件循环分为6个阶段,这些阶段是按照图中顺序依次执行的。每个阶段都有自己的一个队列,存放着相应的任务,每执行一个阶段都会遍历这个阶段里面的任务,依照先进先出原则执行,只有当一个阶段的任务都执行完之后才会执行下一个阶段。这里需要注意的是 nextTickQueue 跟 microTaskQueue 是在每个阶段的每个任务执行的时候都会区执行的。也就是说如果 Timer 队列里面有三个任务, 则每执行一个任务都会去判断 nextTickQueue 跟 microTaskQueue 是否有任务,有的话先执行,执行完之后再执行下一个任务。如果这两个队列一直执行不完,就会导致后续的任务阻塞,最后系统崩溃。所以我个人理解为 microTask 是依附在 macroTask 的,每个 macroTask 都可能包含自己的 microTask,只有当当前 macroTask 附属的microTask 执行完之后才会执行下一个 macroTask。
9. 下面简单说下每个阶段分别是什么:
1). Timer 阶段:定时器阶段,主要处理setTimeout/setInterval 回调函数。当进入这个阶段的时候,主线程会判断当前时间是否满足定时器要求,即定时器设置的延迟时间是否到了,是的话就执行,否则进入下一个阶段。
2). Pending I/O callbacks 阶段:IO回调阶段,处理IO回调函数。
3). Idle, prepare 阶段:node 内部操作,具体也不清楚
4). Poll 阶段:轮询,等待还未返回的IO事件
5). Check 阶段:执行 setImmediate 回调。
6). Close callbacks:执行关闭请求的回调,比如 socket.on(‘close‘, () => {})
通过这6个阶段的大概说明,我们可以知道,其实当 setTimeout 延迟时间为0(理想状态下) 的时候,其实是优先执行于 setImmediate 的,但是在实际操作中,这点是不好保证的,因为setTimeout 可能会有一点延迟,导致在 Timer 阶段这个回调函数还未进去队列,所以 setImmediate 会优先执行于 setTimeout。这取决于机器性能跟其他一些不可控因素。但是以下代码却能确保 setImmediate 是优先于 setTimeout 的,为啥?因为 setImmediate 是处于第五阶段,而 IO 回调是第二阶段,setTimeout 是第一阶段。当执行IO 回调的时候,第一阶段(setTimeout)已经跑完了,按顺序第五阶段(setImmediate)会先跑。
1 fs.readFile(‘./test1.js‘, ‘utf-8‘, () => { 2 setTimeout(() => { 3 console.log(‘setTimeout in IO callback‘) 4 }) 5 setImmediate(() => { 6 console.log(‘setImmediate in IO callback‘) 7 process.nextTick(() => { 8 console.log(‘process in IO-setImmediate callback‘) 9 }) 10 }) 11 process.nextTick(() => { 12 console.log(‘process in IO callback‘) 13 }) 14 console.log(‘sync in IO callback‘) 15 }) 16 // 输出结果: 17 // sync in IO callback 18 // process in IO callback 19 // setImmediate in IO callback 20 // process in IO-setImmediate callback 21 // setTimeout in IO callback
setImmediate 跟 setTimeout 比较:setTimeout 会计算当前任务是否可执行,可以才执行。而 setImmediate 不需要计算,而是直接放入队列中,所以 setImmediate 性能相对于 setTimeout 会好一些
原文:https://www.cnblogs.com/l-c-blog/p/11746410.html