Event Loop 我感觉我又行了(Node.js篇)
我们现在只讨论 Node.js v11+ 版本。
相对于浏览器的 Event Loop 来说,在 Node.js 中的 Event Loop 反而更加简洁。
The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.
Node.js 的事件循环,它会把一些操作放到其他相关的线程来处理,当处理完毕之后,会通知主线程,由主线程决定什么时候来执行。
也就是说:
- Node.js 是通过事件循环机制来运行 JS 代码的;
- 提供了线程池处理 I/O 操作任务;
- 两种线程:
- 事件循环线程:负责安排任务(require、同步执行回调、注册新任务);
- 线程池(libuv实现),负责处理任务(I/O操作、CPU密集型任务)。
事件循环阶段 phase
- Timers:setTimeout / setInterval;
- Pending callbacks:执行延迟到下一个事件环迭代的 I/O 回调(内部机制使用);
- Idle,prepare:系统内部机制使用;
- Poll:轮循,检查新的 I/O 事件;执行 I/O 的回调;(几乎所有情况下,除了关闭的回调函数,那些由计时器和
setImmediate()
调度的之外),其余情况 node 将在适当的时候在此阻塞。 - Check:setImmediate;
- Close callbacks:关闭回调函数(内部机制使用)。
在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。
举个粟子
js
// promise1.then
Promise.resolve().then(() => {
console.log(1)
})
// nextTick1
process.nextTick(() => {
console.log(2)
})
console.log('start')
// readFile
readFile('1.txt', 'utf-8', () => {
// setTimeout1
setTimeout(() => {
console.log(3)
}, 0)
// nextTick2
process.nextTick(() => {
console.log(4)
})
// setImmediate1
setImmediate(() => {
console.log(5)
})
console.log(6)
})
console.log(7)
// setTimeout2
setTimeout(() => {
console.log(8)
}, 0)
// setImmediate2
setImmediate(() => {
console.log(9)
})
console.log('end')
-
代码执行;
-
promise1.then
的回调放入微任务队列; -
nextTick1
的回调放入微任务队列(这里稍候会有解释); -
console.log('start')
执行,输出 start; -
readFile
放入 Poll; -
console.log(7)
执行,输出 7; -
setTimeout2
放入 Timers; -
setImmediate2
放入 Check; -
console.log('end')
执行,输出end
; -
主执行栈清空,准备进入 Timers 阶段,先执行
nextTick1
的回调,输出 2; -
nextTick1
的回调执行完毕,清空微任务promise1.then
,输出 1; -
进入 Timers 阶段,
setTimeout2
执行,setTimeout2
的回调放入回调队列; -
进入 Poll 阶段;
-
注意这个阶段它有短暂的等待时间,如果在等待时间内 Timers 到时,会先回去执行 Timers
setTimeout2
的回调,输出 8;然后再回到 Poll,再进行 Check 执行setImmediate2
的回调,输出 9; -
它也有可能在等待时间结束后,Timers 还没到时,会先进入 Check 阶段,执行
setImmediate2
的回调,输出 9;然后回去执行setTimeout2
的回调,输出 8;
-
-
下一轮没有了微任务, Timers,为空,直接进入 Poll,
readFile
的回调执行;setTimeout1
放入 Timers;nextTick2
的回调放入微任务队列;setImmediate1
放入 Check;console.log(6)
执行,输出 6; -
主执行栈执行完毕,
nextTick2
的回调执行,输出4 -
Poll 阶段完毕,注意,在 I/O 操作里面,会直接进入 Check 阶段,
setImmediate1
执行,输出 5;setTimeout1
执行,输出 3; -
完事。
所以这个例子的执行结果有两种:start 7 end 2 1 9 8 6 4 5 3
或者 start 7 end 2 1 8 9 6 4 5 3
。
再举个粟子
js
console.log('start')
readFile('1.txt', 'utf-8', () => {
setTimeout(() => {
// nextTick1
process.nextTick(() => {
console.log('nextTick1')
})
console.log('setTimeout')
})
Promise.resolve().then(() => {
// nextTick2
process.nextTick(() => {
console.log('nextTick2')
})
console.log('promise')
})
setImmediate(() => {
// nextTick3
process.nextTick(() => {
console.log('nextTick3')
})
console.log('setImmediate')
})
// nextTick4
process.nextTick(() => {
console.log('nextTick4')
})
console.log('readFile')
})
console.log('end')
我们看一下这个例子:
-
start 和 end 先输出,这应该没什么问题;
<主执行栈>:
start
end
: <微任务>:
: : readFile
: <回调队列>:
-
readFile
回调执行,setTimeout
放入 Timers;promise.then
的回调放入微任务队列;setImmediate
放入 Check;nextTick4
的回调挂起;console.log('readFile')
执行,输出 readFile;<主执行栈>:
start
end
readFile
: nextTick4 的回调
<微任务>:
promise.then 的回调
: setTimeout
: readFile
: setImmediate
<回调队列>:
-
nextTick4
回调执行,输出 nextTick4;<主执行栈>:
start
end
readFile
nextTick4
: nextTick4 的回调<微任务>:
promise.then 的回调
: setTimeout
: readFile
: setImmediate
<回调队列>:
-
清空微任务,
promise.then
回调执行,nextTick2
的回调挂起,console.log('promise')
执行,输出 promise;<主执行栈>:
start
end
readFile
nextTick4
promise
<nextTick 的回调>:
nextTick4 的回调nextTick2 的回调
<微任务>:
promise.then 的回调: setTimeout
: readFile
: setImmediate
<回调队列>:
-
nextTick2
回调执行,输出 nextTick2;<主执行栈>:
start
end
readFile
nextTick4
promise
nextTick2
: nextTick4 的回调nextTick2 的回调<微任务>:
promise.then 的回调: setTimeout
: readFile
: setImmediate
<回调队列>:
-
Timer 阶段,
setTimeout
执行,回调挂起;<主执行栈>:
start
end
readFile
nextTick4
promise
nextTick2
: nextTick4 的回调nextTick2 的回调<微任务>:
promise.then 的回调: setTimeout: readFile
: setImmediate
<回调队列>:
setTimeout 的回调
-
Poll 阶段,去检查 Check,
setImmediate
回调执行,nextTick3
的回调挂起;console.log('setImmediate')
,输出 setImmediate;<主执行栈>:
start
end
readFile
nextTick4
promise
nextTick2
setImmediate
: nextTick4 的回调nextTick2 的回调nextTick3 的回调
<微任务>:
promise.then 的回调: setTimeout: readFile: setImmediate<回调队列>:
setTimeout 的回调
-
nextTick3
的回调执行,输出 nextTick3;<主执行栈>:
start
end
readFile
nextTick4
promise
nextTick2
setImmediate
nextTick3
: nextTick4 的回调nextTick2 的回调nextTick3 的回调<微任务>:
promise.then 的回调: setTimeout: readFile: setImmediate<回调队列>:
setTimeout 的回调
-
setTimeout
的回调执行,nextTick1
的回调挂起;console.log('setTimeout')
执行,输出 setTimeout;<主执行栈>:
start
end
readFile
nextTick4
promise
nextTick2
setImmediate
nextTick3
setTimeout
: nextTick4 的回调nextTick2 的回调nextTick3 的回调nextTick1 的回调
<微任务>:
promise.then 的回调: setTimeout: readFile: setImmediate<回调队列>:
setTimeout 的回调 -
nextTick1
的回调执行,输出 nextTick1;<主执行栈>:
start
end
readFile
nextTick4
promise
nextTick2
setImmediate
nextTick3
setTimeout
nextTick1
: nextTick4 的回调nextTick2 的回调nextTick3 的回调nextTick1 的回调<微任务>:
promise.then 的回调: setTimeout: readFile: setImmediate<回调队列>:
setTimeout 的回调 -
完事。
理解 process.nextTick()
需要注意的是,在 Node.js 的文档中指出,process.nextTick()
从技术上来讲,它不属于事件循环的一部分。
它会在当前阶段的操作完成之后处理 nextTick队列
。
任何在给定阶段中调用的 process.nextTick()
,它的回调都会在事件循环继续之前执行。可以这样理解,它在当前阶段的所有同步代码执行完毕之后,立即执行。
就如同我们第2个例子,每个 process.nextTick()
的回调都会在执行完当前阶段的代码,进入下个阶段之前被执行。
我们再分析一下下面的例子:
js
console.log(1)
// nextTick1
process.nextTick(() => {
console.log(2)
// nextTick2
process.nextTick(() => {
console.log(3)
})
})
new Promise((resolve, reject) => {
// nextTick3
process.nextTick(() => {
console.log(4)
// nextTick4
process.nextTick(() => {
console.log(5)
})
})
console.log(6)
resolve()
}).then(() => {
console.log(7)
})
- 代码执行,
console.log(1)
执行,输出1; nextTick1
放入nextTick队列
;- Promise 的 Executor 执行,
nextTick3
放入nextTick队列
;console.log(6)
执行,输出6;Promise 状态变更,promise.then
的回调放入微任务队列; - 主执行栈完毕,
nextTick1
的回调执行,console.log(2)
执行,输出 2;nextTick2
放入nextTick队列
; nextTick3
的回调执行,console.log(4)
执行,输出 4;nextTick4
放入nextTick队列
;nextTick2
的回调执行,console.log(3)
执行,输出 3;nextTick4
的回调执行,console.log(5)
执行,输出 5;- 清空微任务,
console.log(7)
执行,输出 7; - 完事。
以上,Node.js 的事件环相对来讲还是比较简单的,可能理解起来会比较绕。