事件循环:宏任务+微任务

98 阅读3分钟

概念:什么是事件循环

事件循环 的概念非常简单。它是一个在 JavaScript 引擎等待任务,执行任务和进入休眠状态等待更多任务这几个状态之间转换的无限循环。

事件循环一般算法 (没有微任务的情况)

  1. 有任务的时候,按入队出队执行

  2. 队空休眠,不空转到1. 注意:

  • 引擎执行任务时永远不会进行渲染(render)。如果任务执行需要很长一段时间也没关系。仅在任务完成后才会绘制对 DOM 的更改。
  • 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。因此,在一定时间后,浏览器会抛出一个如“页面未响应”之类的警报,建议你终止这个任务。这种情况常发生在有大量复杂的计算或导致死循环的程序错误时。

setTimeout(f)

安排(schedule)一个新的 宏任务

  • 使用零延迟的 setTimeout(f)

它可以把场任务拆分成多个部分,让浏览器对用户事件及时反馈

微任务在下个宏任务之前执行

异步任务需要适当的管理。ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8 术语)。

  • Promise 的处理程序(handlers).then.catch 和 .finally 都是异步的。
  • queueMicrotask(func),它对 func 进行排队,以在微任务队列中执行。如果我们想要异步执行(在当前代码之后)一个函数,但是要在更改被渲染或新事件被处理之前执行,那么我们可以使用 queueMicrotask 来对其进行安排(schedule)。

每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再执行其他的宏任务,或渲染,或进行其他任何操作。

事件循环真实算法

  1. 宏任务队列,出队执行
  2. 执行所有的微任务队列
  3. 渲染变更
  4. 宏任务队列继续循环

一道例题

下方这段代码的输出是什么?

console.log(1); 
setTimeout(() => console.log(2)); 
Promise.resolve().then(() => console.log(3)); 
Promise.resolve().then(() => setTimeout(() => console.log(4)));
Promise.resolve().then(() => console.log(5)); setTimeout(() => console.log(6));
console.log(7);

输出结果为:1 7 3 5 2 6 4。

这道题其实很简单,我们只需要知道微任务和宏任务队列是如何工作的。

让我们一起一步一步地看看发生了什么。

console.log(1);
// 第一行立即执行,它输出 `1`。
// 到目前为止,宏任务队列和微任务队列都是空的。 
setTimeout(() => console.log(2));
// `setTimeout` 将回调添加到宏任务队列。 
// - 宏任务队列中的内容:
// `console.log(2)` 
Promise.resolve().then(() => console.log(3));
// 将回调添加到微任务队列。
// - 微任务队列中的内容:
// `console.log(3)` 
Promise.resolve().then(() => setTimeout(() => console.log(4))); 
// 带有 `setTimeout(...4)` 的回调被附加到微任务队列。
// - 微任务队列中的内容:
// `console.log(3); setTimeout(...4)` 
Promise.resolve().then(() => console.log(5));
// 回调被添加到微任务队列 
// - 微任务队列中的内容:
// `console.log(3); setTimeout(...4); console.log(5)` 
setTimeout(() => console.log(6)); 
// `setTimeout` 将回调添加到宏任务队列 // - 宏任务队列中的内容:
// `console.log(2); console.log(6)` console.log(7); 
// 立即输出 7

总结一下:

  1. 立即输出数字 1 和 7,因为简单的 console.log 调用没有使用任何队列。

  2. 然后,主代码流程执行完成后,开始执行微任务队列。

    • 其中有命令行:console.log(3); setTimeout(...4); console.log(5)
    • 输出数字 3 和 5setTimeout(() => console.log(4)) 将 console.log(4) 调用添加到了宏任务队列的尾部。
    • 现在宏任务队列中有:console.log(2); console.log(6); console.log(4)
  3. 当微任务队列为空后,开始执行宏任务队列。并输出 26 和 4

最终,我们的到的输出结果为:1 7 3 5 2 6 4