看了JSConf上一个大神介绍Event Loop的视频讲得非常好,给它总结一下:地址

JS异步机制

这张图可以帮助我们理解:

  • JS 运行在单线程上,通过 event loop 完成异步调度
  • DOM、XMLHTTPREquest、setTimeout、setInterval 等是异步的 WebAPIs
  • 异步代码会被推入 callback queue(Tasks)中,当 stack 空闲时挨个推入 stack

这里是视频中关于执行一个异步操作时的图示:

  • 当浏览器开始执行我们的代码时,会将代码推入 Tasks 中
  • 当 stack 为空,则 event loop 从 task queue 中取出一个任务执行
  • 运行代码打印Hi
  • 遇到setTimeout时,将回调推入 webapis 管理(里面有一个定时队列)
  • 代码继续向下执行,输出JSConfEU,随后 stack 清空,UI 刷新
  • 当定时时间到了,webapis 将回调推入 task queue 中,stack 此时为空,event loop 取一个任务推入 stack 执行
  • 代码输出there

以上就是 JS 的异步执行过程,Tasks就是上图中的 task queue,下面再看看microtasks

Tasks/microtasks 的区别

这里有一篇Google Chrome 的开发者对microtasks的详细介绍,地址,可以抽时间读一下。

代码解读 1:

setTimeoutPromise同时存在时,代码示例及执行顺序说明如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
console.log("script start");

setTimeout(function() {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  });

console.log("script end");

下图中需要重点理解 Tasks、Microtasks、JS stack 这三个栈

第一次 Task 执行:

  • 将当前的 script 推入 Tasks 中
  • 执行第一段代码,输出script start
  • setTimout 回调推入 webapis 的定时栈中
  • Promise 的第一个 then 回调推入 Microtasks
  • 执行最后一句输出script end
  • 检查 Microtasks 中是否有任务
  • 执行推入的第一个 Promise 的 then 回调
  • 输出promise1,之后将第二个 then 回调推入 Microtasks
  • 检查 Microtasks 中是否有任务
  • 执行推入的第二个 Promise 的 then 回调
  • 输出promise2
  • 第一个 Tasks 执行完毕,刷新 UI

第二次 Task 执行:

  • webapis 根据定时将 setTimeout 回调推入 Tasks 中
  • Event loop 从 Tasks 取出任务执行
  • 输出setTimeout
  • 检查 Microtasks 中没有任务
  • 当前 Task 执行完毕,刷新 UI

Tasks 和 microtasks 所涉及的 API:

  • Tasks: setTimeout, setInterval, (setImmediate, I/O,) UI rendering
  • microtasks: (process.nextTick,) Promise, MutationObserver

需要注意的地方:

  • microtasks 不是 webapis,他的优先级比 Tasks 高,每轮主流任务执行完毕都会检查 microtasks,检查完毕后这轮 Task 才结束
  • micrtasks 为 js 逻辑的一部分,不涉及到 UI 等 web 功能,因此功能上应该和 setTimeout 等 webapis 作区分
  • 每轮 Task 结束都会渲染 UI,为了防止 UI 卡死
  • setTimeout 由 webapis 管理,当定时时间到了,就推入 Tasks 中等待执行(不是立即执行)
  • setTimeout 最短执行时间为 4ms,即使定时时间为 0

代码解读 2

两个父子关系的 div 绑定 click 事件,根据回调输出 log。

1
2
3
<div class="outer">
  <div class="inner"></div>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Let's get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log("mutate");
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log("click");

  setTimeout(function() {
    console.log("timeout");
  }, 0);

  Promise.resolve().then(function() {
    console.log("promise");
  });

  outer.setAttribute("data-random", Math.random());
}

// …which we'll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);

点击内部 div 后的执行过程如下:

脚本执行:

  • 将 script 推如 Tasks,因为 stack 空闲,执行 script 脚本,完成事件绑定

用户点击:

  • 用户点击 inner div
  • 因为是在冒泡阶段触发,因此 inner 元素的 click 事件先触发,回调推入 Tasks 中
  • 冒泡向上,outer 元素的 click 事件触发,回调推入 Tasks 中
  • 当前没有 microtasks,Tasks 执行完毕,UI 更新

第一个事件回调(inner):

  • 从 Tasks 中取出任务(inner 元素的事件回调)
  • 打印click
  • setTimout 回调推入 webapis 的定时栈中
  • 将 Promise 的第一个 then 推入 microtasks 中
  • 修改 outer 的属性触发监听,将 MutationObserver 推入 microtasks
  • 检查 microtasks 中任务并执行
  • 打印 promisemutate
  • 第一个回调执行完毕,执行第二个 click 事件回调

第二个事件回调(outer):

  • 从 Tasks 中取出任务(outer 元素的事件回调)
  • 打印click
  • setTimout 回调推入 webapis 的定时栈中
  • 将 Promise 的第一个 then 推入 microtasks 中
  • 修改 outer 的属性触发监听,将 MutationObserver 推入 microtasks
  • 检查 microtasks 中任务并执行
  • 打印 promisemutate
  • 第二个回调执行完毕

setTimeout 回调:

  • 上两个 Tasks 执行完毕,检查 webapis 是否有定时任务
  • 将 inner 中的 setTimeout 推入 Tasks
  • 执行回调输出timeout
  • 检查 webapis 是否有定时任务
  • 将 outer 中的 setTimeout 推入 Tasks
  • 执行回调输出timeout

需要注意的地方

  • 注意冒泡过程添加 Tasks 的顺序
  • Tasks 中的任务遵循先进先出的规则
  • microtasks 中的任务在每次 Tasks 时都必须检查并执行完毕