Event Loop
文章目录

从一道面试题开始

setTimeout的对代码输出的影响

先来看个例子:

1
2
3
4
5
window.setTimeout(function() {
console.log(2)
}, 1000);

console.log(1)

输出: 输出1, 然后一秒后输出2

很容易理解因为延迟了1秒

但是如果将延迟时间改为0

1
2
3
4
5
window.setTimeout(function() {
console.log(2)
}, 0);

console.log(1)

输出依然相同

几个要点

  1. JS是单线程,异步回调会放在消息队列
  2. 如果当前线程已经没有后续任务,事件循环会拉取消息队列中的下一条信息进行处理
  3. setTimeout 会在计时到达的时候, 添加一条消息(并随着回调函数)一同放到消息队列的末尾
1
2
3
4
5
6
var fuc = [1, 2, 3];
for (var i in fuc) {
setTimeout(function() {
console.log(fuc[i])
}, 1 console.log(fuc[i]);
}

因此上面这段代码的输出就很容易理解了

  1. 执行一次 setTimeout , 开始计时
  2. 然后输出1
  3. 执行第二次 setTimeout , 开始计时
  4. 然后输出2
  5. 执行第三次 setTimeout , 开始计时
  6. 然后输出3
  7. 无论之前的输出花费了多少时间,只要执行这一步的时候和第一步开始计时的时候相差超过1毫秒,那么就开始输出第一次回调
  8. 只要这一步和第三步开始计时的时候相差超过1毫秒,那么就开始输出第二次回调
  9. 只要这一步和第五步开始计时的时候相差超过1毫秒,那么就开始输出第二次回调

另外因为1毫秒非常简短, 因此输出123之后会立刻输出三个3, 因为setTimeout形成了一个闭包

理解了上面这段, 就可以发现下面这段代码已经死锁了.

1
2
3
4
5
6
7
8
9
var t = true;

window.setTimeout(function() {
t = false;
}, 1000);

while (t) {}

alert('end');

Promise的乱入

同样一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout(function() {
console.log(4)
}, 0);
new Promise(function(resolve) {
console.log(1)
for (var i = 0; i < 10000; i++) {
i == 9999 && resolve()
}
console.log(2)
}).then(function() {
console.log(5)
});
console.log(3);

输出是12354

关于这个输出, 有如下几个逻辑:

2.4是 setTimeOut.callback 的输出, 加入 MacroTask 末端,

  1. 输出1

  2. 执行 Promise.resolve() 将输出5的callback放到 MicroTask 中(注意这里不是 MacroTask )

  3. 输出2

  4. 输出3

  5. MacroTask 首个任务执行完毕

  6. 查找 MicroTask 里面有没有任务, 发现有, 执行, 输出5

  7. 查找 MacroTask 里面有没有任务, 发现有, 执行, 输出4

  8. 查找 MicroTask 里面有没有任务, 发现没有, 可以休息了

  9. 查找 MacroTask 里面有没有任务, 发现没有, 可以睡觉了

  10. 执行完毕

关于 macrotask 和 microtask

简介

一个事件循环(EventLoop)中会有一个正在执行的任务(Task), 而这个任务就是从 macrotask 队列中来的. 在whatwg规范中有 queue 就是任务队列. 当这个 macrotask 执行结束后所有可用的 microtask 将会在同一个事件循环中执行, 当这些 microtask 执行结束后还能继续添加 microtask 一直到真个 microtask 队列执行结束.

怎么用

基本来说, 当我们想以同步的方式来处理异步任务时候就用 microtask(比如我们需要直接在某段代码后就去执行某个任务, 就像Promise一样).

其他情况就直接用 macrotask.

两者的具体实现

  • macrotasks: setTimeout setInterval setImmediate I/O UI渲染
  • microtasks: Promise process.nextTick Object.observe MutationObserver

从规范中理解

whatwg规范:https://html.spec.whatwg.org/multipage/webappapis.html#task-queue

  • 一个事件循环(event loop)会有一个或多个任务队列(task queue) task queue 就是 macrotask queue
  • 每一个 event loop 都有一个 microtask queue
  • task queue == macrotask queue != microtask queue
  • 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
  • 当一个 task 被放入队列 queue(macro或micro) 那这个 task 就可以被立即执行了

再来回顾下事件循环如何执行一个任务的流程

当执行栈(call stack)为空的时候, 开始依次执行:

  1. 把最早的任务(task A)放入任务队列

  2. 如果 task A 为null (那任务队列就是空),直接跳到第6步

  3. 将 currently running task 设置为 task A

  4. 执行 task A (也就是执行回调函数)

  5. 将 currently running task 设置为 null 并移出 task A

  6. 执行 microtask 队列

    1. 在 microtask 中选出最早的任务 task X
    2. 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
    3. 将 currently running task 设置为 task X
    4. 执行 task X
    5. 将 currently running task 设置为 null 并移出 task X
    6. 在 microtask 中选出最早的任务 , 跳到 b
    7. 结束 microtask 队列
  7. 跳到第一步

上面就算是一个简单的 event-loop 执行模型

再简单点可以总结为:

  1. 在 macrotask 队列中执行最早的那个 task ,然后移出
  2. 执行 microtask 队列中所有可用的任务, 然后移出
  3. 下一个循环,执行下一个 macrotask 中的任务 (再跳到第2步)

其他

  1. 当一个task(在 macrotask 队列中)正处于执行状态,也可能会有新的事件被注册,那就会有新的 task 被创建。比如下面两个

    1. promiseA.then() 的回调就是一个 task
    1. promiseA 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
    1. promiseA 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
    1. setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
  2. microtask queue 中的 task 会在事件循环的当前回合中执行,因此 macrotask queue 中的 task 就只能等到事件循环的下一个回合中执行了

  3. click ajax setTimeout 的回调是都是 task, 同时,包裹在一个 script 标签中的js代码也是一个 task 确切说是 macrotask。

参考文献

https://github.com/ccforward/cc/issues/48
https://www.zhihu.com/question/36972010