Node.js与浏览器事件循环差异:为何这段异步代码输出不同?
Node.js与浏览器事件循环差异解析(针对给定异步代码)
先把咱们要分析的代码贴出来,方便对照:
async function async1() { console.log("a"); await async2(); console.log("b"); } async function async2() { console.log( 'c'); } console.log("d"); setTimeout(function () { console.log("e"); },0); async1(); new Promise(function (resolve) { console.log("f"); resolve(); }).then(function () { console.log("g"); }); console.log('h');
先直接看两个环境的输出差异:
- Node.js 运行时输出:
d a c f h b g e - 谷歌浏览器运行时输出:
d a c f h g b e
差异点很明显:b和g的输出顺序反过来了。咱们一步步拆解,看看问题出在哪。
第一步:同步代码执行阶段(两个环境完全一致)
不管是Node还是浏览器,同步代码都是按从上到下的顺序执行,这部分输出没有区别:
- 首先执行
console.log("d")→ 输出d - 调用
setTimeout,它的回调会被放进宏任务队列(宏任务优先级最低,最后才会执行) - 调用
async1():- 先执行
console.log("a")→ 输出a - 接着调用
async2(),里面的console.log("c")同步执行 → 输出c;因为async2是async函数,默认返回一个已经resolved的Promise - 遇到
await,async1会暂停执行,后续的console.log("b")会被包装成一个回调,等着被放进微任务队列
- 先执行
- 执行
new Promise的构造函数:- 同步执行
console.log("f")→ 输出f - 调用
resolve()后,then里的console.log("g")会被放进微任务队列
- 同步执行
- 最后执行
console.log('h')→ 输出h
到这里同步代码就跑完了,接下来就是两个环境差异的核心:微任务队列的处理逻辑不一样。
第二步:微任务处理的核心差异
Node.js环境(以v10及旧版本为例,新版本优先级逻辑未变)
Node.js把微任务分成了两个优先级不同的队列:
process.nextTick队列:优先级最高,只要队列里有任务,就会先把它执行完- Promise队列:也就是
then/catch/finally的回调队列,优先级比前者低
在这段代码里,await后续的console.log("b")会被放到process.nextTick队列,而new Promise的then回调console.log("g")会放到Promise队列。所以微任务的执行顺序是:
- 先把
process.nextTick队列的任务跑完 → 输出b - 再执行Promise队列的任务 → 输出
g
浏览器环境
浏览器的微任务队列不分优先级,完全按照先进先出的顺序执行。
这里的关键是:await的后续回调是在new Promise的then回调之后才加入队列的。为啥?因为当await遇到已经resolved的Promise时,浏览器会先把当前所有同步代码执行完,再把await的后续回调塞进微任务队列;而new Promise的then回调是在调用resolve()的瞬间就被加入队列了。
所以微任务队列的顺序是:
- 先执行先加入的
console.log("g")→ 输出g - 再执行后加入的
console.log("b")→ 输出b
第三步:宏任务收尾
等所有微任务都执行完,才会轮到宏任务队列里的setTimeout回调,执行后输出e。
内容的提问来源于stack exchange,提问作者hui




