We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0)
由Javascript零碎之事件循环机制 可以很简单的得到结果:
然而,相同的代码,在node中运行后却得到了不一样的结果:
为什么同样都是单线程的事件循环机制有这么大的差异呢?
当Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步API行为调用, 使用定时器任务或者nexTick, 处理完成后进入事件循环处理过程
每一个阶段都有一个FIFO的callbacks队列, 每个阶段都有自己的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段.
timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 的执行顺序是不确定的。
setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') })
技术上来说,poll 阶段控制 timers 什么时候执行。
但是如果把它们放到一个I/O回调里面(比如fs.readFile的回调),就一定是 setImmediate() 先执行,因为poll阶段后面就是check阶段。
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行. 名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.
poll 阶段主要有2个功能:
even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:
1.若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列 2.若没有预设的setImmediate(),event loop将阻塞在该阶段等待
注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。
setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。
如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发
const fs = require('fs') fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
由于是在IO的回调里,所以会立马去到poll阶段进行处理(I/O回调会由poll阶段处理). 得到如下执行结果:
readFile immediate timeout
在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
回到开头的例子,现在应该能清楚的知道为什么输出结构会和浏览器上的有那么大的差别了:
全局脚本(main())执行,将2个timer依次放入timer队列,main()执行完毕,调用栈空闲,任务队列开始执行; 首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2; 至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.它一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段。
而setImmediate的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.
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()
结果是:
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
遇到IO回调会去到poll阶段,然后会查看是否有setImmediate,发现有,会先执行setImmediate,在某一间隙,setImmediate执行完了后,poll会执行IO回调,然后继续往下。 所以递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()。
深入理解js事件循环机制(Node.js篇 nodejs理解事件循环机制
The text was updated successfully, but these errors were encountered:
No branches or pull requests
从一个例子开始
由Javascript零碎之事件循环机制 可以很简单的得到结果:

然而,相同的代码,在node中运行后却得到了不一样的结果:

为什么同样都是单线程的事件循环机制有这么大的差异呢?
Node.js的事件循环机制
当Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步API行为调用, 使用定时器任务或者nexTick, 处理完成后进入事件循环处理过程
事件循环阶段
每一个阶段都有一个FIFO的callbacks队列, 每个阶段都有自己的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段.
timers 阶段
timers 是事件循环的第一个阶段,Node 会去检查有无已过期的timer,如果有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会立即执行,因为Node对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 的执行顺序是不确定的。
但是如果把它们放到一个I/O回调里面(比如fs.readFile的回调),就一定是 setImmediate() 先执行,因为poll阶段后面就是check阶段。
I/O callbacks阶段
这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED,
类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行.
名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.
poll 阶段
poll 阶段主要有2个功能:
even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:
1.若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
2.若没有预设的setImmediate(),event loop将阻塞在该阶段等待
注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段。
check 阶段
setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。
close callbacks 阶段
如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发
例子说明
由于是在IO的回调里,所以会立马去到poll阶段进行处理(I/O回调会由poll阶段处理).
得到如下执行结果:
node中的macro task 和 micro task
在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
开头例子的说明
回到开头的例子,现在应该能清楚的知道为什么输出结构会和浏览器上的有那么大的差别了:
全局脚本(main())执行,将2个timer依次放入timer队列,main()执行完毕,调用栈空闲,任务队列开始执行;
首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段之前,执行microtask队列的所有任务,依次打印promise1、promise2。
process.nextTick() VS setImmediate()
process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.它一旦执行,要直到nextTick队列被清空,才会进入到下一个事件阶段。
而setImmediate的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.
结果是:
替换成setImmediate(),运行结果:
遇到IO回调会去到poll阶段,然后会查看是否有setImmediate,发现有,会先执行setImmediate,在某一间隙,setImmediate执行完了后,poll会执行IO回调,然后继续往下。
所以递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()。
参考
深入理解js事件循环机制(Node.js篇
nodejs理解事件循环机制
The text was updated successfully, but these errors were encountered: