原稿:语雀 · Event Loop详解 · 原目录:JavaScript › Event Loop详解
红宝书异步部分对于一些更底层的异步机制没有详细解释,其中最重要的就是事件循环,整合总结在这
1. 同步&异步
这里不深入同步异步内容,只是给出最简单的说明:
- 同步(sync)即一个步骤接一个步骤完成:
A()执行-A()结束-B()执行,如果A()不结束就会一直阻塞下去
- 异步(async)与同步相对,无需严格挨个执行:
A()执行-A()挂起-B()执行-B()结束-A()执行-A()结束
JS异步必要性
场景:用户在语雀在编辑文档过程中点击“保存”,如果只能同步操作,只要保存操作没有结束,用户不能执行别的操作,无法继续编辑,极其影响体验
JS作为单线程语言,引擎只能按顺序往下执行代码,不能并行执行任务,遇到耗时任务时,用户必须长时间等待(阻塞),此时异步就很有必要
2. Event Loop
JS本身是一门单线程语言,为了实现异步,它遵循了一种机制——单线程事件循环模型
JS引擎执行任务时会先分类后调度:
| ![]() |
2.1 宏任务&微任务
JS中的任务会被分为两种
- 宏任务(MacroTask、Task),script所有代码、setTimeout、setInterval、系统IO、UI渲染
- 微任务(MicroTask),如promise
那么考虑到宏、微任务后的分类调度过程如下:
- 同步任务:压入执行栈由主线程执行
- 异步任务:则会在异步结果就绪后将其注册的回调按照宏、微任务分类放入不同的任务队列
2.2 事件循环进程模型
宏任务与微任务在调度过程中的区别就是执行方式与优先级:
栈中任务 > 微任务队列(依次执行所有就绪任务) > 宏任务(每次执行一项就绪任务)
2.3 实例分析
例1
setTimeout(() => {
console.log("我是第一个宏任务");
Promise.resolve().then(() => {
console.log("我是第一个宏任务里的第一个微任务");
});
Promise.resolve().then(() => {
console.log("我是第一个宏任务里的第二个微任务");
});
}, 0);
setTimeout(() => {
console.log("我是第二个宏任务");
}, 0);
Promise.resolve().then(() => {
console.log("我是第一个微任务");
});
console.log("执行第一个同步任务");
console.log("执行第二个同步任务");
执行过程:

执行栈空闲; 2. 当前微任务队列有一个promise then回调,入栈执行,打印"我是第一个微任务";
微任务队列变空;
执行栈空闲; 2. 宏任务队列队首是一个setTimeout回调,入栈执行,打印"我是第一个宏任务";两次promise.resolve()立即落定,两个promise then回调进入微任务队列;
执行栈空闲; 2. 当前微任务队列有两个promise then()回调,依次入栈执行,打印"我是第一个宏任务里的第一个微任务"、“我是第一个宏任务里的第二个微任务”;
微任务队列变空;
执行栈空闲; 2. 宏任务队列队首是一个setTimeout回调,入栈执行,打印"我是第二个宏任务";
执行栈空闲; 2. 此时宏任务队列也被清空,所有任务执行完毕
例2:与例1对比,将例1的15-17行的promise落定通过setTimeout推迟,对应本例15-22行
setTimeout(() => { console.log("我是第一个宏任务"); Promise.resolve().then(() => { console.log("我是第一个宏任务里的第一个微任务"); }); Promise.resolve().then(() => { console.log("我是第一个宏任务里的第二个微任务"); });}, 0);setTimeout(() => { console.log("我是第二个宏任务");}, 0);let p = new Promise((resolve) => setTimeout(() => { console.log("我是第三个宏任务"); resolve(); }, 3000)).then(() => { console.log("我是第一个微任务");});console.log("执行第一个同步任务");console.log("执行第二个同步任务");
执行过程:

3. async/await中的事件循环
ES8新增的async/await关键字实际上是promise的语法糖:
- async函数类似promise的执行函数,其中代码如果没有await则就是同步执行的
- await能实现类似then()中的回调/处理程序,await会暂停执行异步函数后面的代码,让出JS主线程,直到其后promise被解决,才将异步函数剩余任务放回任务队列
3.1 实例分析
异步函数的使用不在这里多介绍,但单独分析异步函数的事件循环还是有必要的:
例1
async function foo(args) {
console.log(args);
console.log(await Promise.resolve("我是foo()的第一个微任务"));
console.log("我是foo()的第二个微任务");
}
async function bar(args) {
console.log(args);
console.log(await "我是bar()的第一个微任务");
console.log("我是bar()的第二个微任务");
}
console.log("第一个同步任务");
foo("第二个同步任务");
console.log("第三个同步任务");
bar("第四个同步任务");
console.log("第五个同步任务");
执行过程

await后的promise立即解决,把给await提供值并恢复foo()执行的回调添加到微任务队列;
foo()退出; 2. 打印"第三个同步任务" 4. 执行async bar(),打印"第四个同步任务";
await后内容被包装为promise且立即解决,把给await提供值并恢复bar()执行的回调添加到微任务队列;
bar()退出; 2. 打印"第五个同步任务" 4. 当前同步任务执行结束,栈空闲,此时微任务队列有两个任务 6. 第一个微任务入栈,await取得解决值"我是foo()的第一个微任务"并打印,foo()恢复执行,打印"我是foo()的第二个微任务" 8. 第二个微任务入栈,await取得解决值"我是bar()的第一个微任务"并打印,bar()恢复执行,打印"我是bar()的第二个微任务" 10. 执行栈空闲,宏任务队列被清空
TC39对await后期约回调如何处理做过一次修改
- 新机制下执行Promise.resolve()后只会生成一个异步任务——向await传值并恢复async函数执行
- 旧机制会先后生成两个异步任务加入任务队列
- 向await传值的回调首先进入任务队列
- 当await收到解决值之后,再生成一个用于恢复async函数的回调进入任务队列
例2:与之前相比,foo()中await后的promise解决后又紧接了一个onResolve()回调
async function foo(args) {
console.log(args);
console.log(
await Promise.resolve("我是foo()的第一个微任务").then((value) => {
console.log(value);
return "我是foo()的第二个微任务";
})
);
console.log("我是foo()的第三个微任务");
}
async function bar(args) {
console.log(args);
console.log(await "我是bar()的第一个微任务");
console.log("我是bar()的第二个微任务");
}
console.log("第一个同步任务");
foo("第二个同步任务");
console.log("第三个同步任务");
bar("第四个同步任务");
console.log("第五个同步任务");
执行过程:

await后的promise立即落定,promise then回调添加到微任务队列;
foo()退出; 2. 打印"第三个同步任务" 4. 执行async bar(),打印"第四个同步任务";
await后内容被包装为promise且立即落定,把给await提供值并恢复bar()执行的回调添加到微任务队列;
bar()退出; 2. 打印"第五个同步任务"
