原稿:语雀 · 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引擎执行任务时会先分类后调度:

  • 同步任务会在调用栈中按照顺序等待主线程依次执行
  • 异步任务会先挂起,在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲时(调用栈被清空),被读取到栈内等待主线程的执行
Event Loop详解 配图

2.1 宏任务&微任务

JS中的任务会被分为两种

  • 宏任务(MacroTask、Task),script所有代码、setTimeout、setInterval、系统IO、UI渲染
  • 微任务(MicroTask),如promise

那么考虑到宏、微任务后的分类调度过程如下:

  • 同步任务:压入执行栈由主线程执行
  • 异步任务:则会在异步结果就绪后将其注册的回调按照宏、微任务分类放入不同的任务队列

2.2 事件循环进程模型

宏任务与微任务在调度过程中的区别就是执行方式与优先级:

栈中任务 > 微任务队列(依次执行所有就绪任务) > 宏任务(每次执行一项就绪任务)

Event Loop详解思维导图

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("执行第二个同步任务");

执行过程:

image.png
2. 执行同步代码,setTimeout计时开始、promise.resolve()、打印"执行第一个同步任务"、"执行第二个同步任务";两个setTimeout回调进入宏任务队列,一个promise then回调进入微任务队列;

执行栈空闲; 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("执行第二个同步任务");

执行过程:

image.png
- 大致与例1一致,但是同步执行15行时遇到setTimeout,3000ms后它的回调进入宏任务队列,该回调进入执行栈后打印"我是第三个宏任务",同时解决p; - p的promise then回调进入微任务队列; - 当setTimeout回调执行完毕退出,微任务队列then回调入栈执行,打印"我是第一个微任务",退出 - 此时宏任务队列也被清空,所有任务执行完毕

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("第五个同步任务");

执行过程

image.png
2. 打印"第一个同步任务" 4. 执行async foo(),打印"第二个同步任务";

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("第五个同步任务");

执行过程:

image.png
2. 打印"第一个同步任务" 4. 执行async foo(),打印"第二个同步任务";

await后的promise立即落定,promise then回调添加到微任务队列;

foo()退出; 2. 打印"第三个同步任务" 4. 执行async bar(),打印"第四个同步任务";

await后内容被包装为promise且立即落定,把给await提供值并恢复bar()执行的回调添加到微任务队列;

bar()退出; 2. 打印"第五个同步任务"