问题
考察如下代码, 脑回路中运行并输出结果:
- console.log("1");
- setTimeout(function setTimeout1() {
- console.log("2");
- process.nextTick(function nextTick1() {
- console.log("3");
- });
- new Promise(function promise1(resolve) {
- console.log("4");
- resolve();
- }).then(function promiseThen1() {
- console.log("5");
- });
- setImmediate(function immediate1() {
- console.log("immediate");
- });
- });
- process.nextTick(function nextTick2() {
- console.log("6");
- });
- function bar() {
- console.log("bar");
- }
- async function foo() {
- console.log("async start");
- await bar();
- console.log("async end");
- }
- foo();
- new Promise(function promise2(resolve) {
- console.log("7");
- resolve();
- }).then(function promiseThen2() {
- console.log("8");
- });
- setTimeout(function setTimeout2() {
- console.log("9");
- new Promise(function promise3(resolve) {
- console.log("11");
- resolve();
- }).then(function promiseThen3() {
- console.log("12");
- });
- process.nextTick(function nextTick3() {
- console.log("10");
- });
- });
JS 事件循环
JS 是单线程, 朴素地讲, 同时只能完成一件事件. 如果有耗时的任务, 那后续的所有任务都要等待其完成才能执行.
为了避免这种阻塞, 引入了事件循环. 即, 将代码的执行分成一个个很小的阶段 (一次循环), 每个阶段重复相应的事情, 直到所有任务都完成.
一个阶段包含以下部分:
Timers: 到期的定时器任务, setTimeout,setInterval 等注册的任务.
IO Callbacks:IO 操作, 比如网络请求, 文件读写.
IO Polling:IO 任务的注册
Set Immediate: 通过 setImmediate 注册的任务
Close:close 事件的回调, 比如 TCP 的断开.
Ticks and Phases of the Node.JS Event Loop 图片来自 Daniel Khan 的 Medium 博客, 见文末
同步代码及上面每个环节结束时都会清空一遍微任务队列, 记住这点很重要!
代码执行流程
执行的流程是,
将代码顺序执行.
遇到异步任务, 将任务压入待执行队列后继续往下.
完成同步代码后, 检查是否有微任务 (通过 Promise,process.nextTick,async/await 等注册), 如果有, 则清空.
清空微任务队列后, 从待执行队列中取出最先压入的任务顺序执行, 重复步骤一.
另,
async/await 本质上是 Promise, 所以其表现会和 Promise 一致.
process.nextTick 注册的回调优先级高于定时器.
setImmediate 可看成 Node 版本的 setTimeout, 所以可与后者同等对待.
示例代码分析
Round 1
首先遇到同步代码 console.log(1), 立即执行输出 1
接下来是一个 setTimeout 定时器, 将其回调压入待执行队列 [setTimeout1]
遇到 process.nextTick, 将其回调 nextTick2 压入微任务队列 [nextTick2]
然后是 async 函数 foo 的调用, 立即执行并输出 async start
然后是 await 语句, 这所在的地方会创建并返回 Promise, 所以这里会执行其后面的表达式, 也就是 bar() 函数的调用.
执行 bar 函数, 输出 bar
在执行了 await 后面的语句后, 它所代表的 Promise 就创建完成了, foo 函数体后续的代码相当于 promise 的 then, 放入微任务队列 [nextTick2, rest_of_foo]
继续往下遇到 new Promise, 执行 Promise 的创建输出 7, 将它的 then 回调压入微任务队列 [nextTick2, rest_of_foo,promiseThen2]
遇到另一个 setTimeout, 回调压入待执行队列 [setTimeout1,setTimeout2]
至此, 代码执行完了一轮. 此时的输出应该是 1, async start, bar,7
Round 2
查看微任务队列, 并清空. 所以依次执行 [nextTick2, rest_of_foo,promiseThen2], 输出 6,async end,8.
Round 3
查看待执行队列 [setTimeout1,setTimeout2], 先执行 setTimout1
遇到 console.log(2) 输出 2
遇到 process.nextTick 将 nextTick1 压入微任务队列 [nextTick1]
遇到 new Promise 立即执行 输出 4, 执行 resolve() 后将 promiseThen1 压入微任务队列 [nextTick1,promiseThen1]
遇到 setImmediate 将回调压入待执行队列 [setTimeout2,immediate1]
此时 setTimeout1 执行完毕, 此时的输出应该为 2,4
Round 4
检查微任务队列 [nextTick1,promiseThen1] 依次执行并输出 3,5
Round 5
检查待执行队列 [setTimeout2,immediate1], 执行 setTimeout2
遇到 console 输出 9
遇到 new Promise 执行并输出 11, 将 promiseThen3 压入微任务队列 [promiseThen3]
遇到 process.nextTick 将 nextTick3 压入微执行队列. 注意, 因为 process.nextTick 的优化级高于 Promise, 所以压入后的结果是: [nextTick3,promiseThen3]
此时 setTimeout2 执行完毕, 输出为 9,11
Round 6
检查微任务队列 [nextTick3,promiseThen3] 执行并输出 10,12
Round 7
检查待执行队列 [immediate1], 执行并输出 immediate
至此, 走完了所有代码.
结果
以下是文章开头的结果:
- 1
- async start
- bar
- 7
- 6
- async end
- 8
- 2
- 4
- 3
- 5
- 9
- 11
- 10
- 12
- immediate
参考
Event Loop 的规范和实现
这一次, 彻底弄懂 JavaScript 执行机制
What you should know to really understand the Node.JS Event Loop
来源: https://www.cnblogs.com/Wayou/p/understanding_event_loop.html