【JS】事件循环,被我这样顿悟了

64 阅读3分钟

明白概念

JavaScript是单线程的语言,从上到下执行,先执行同步任务,再执行异步任务,异步任务分为宏任务(macro)和微任务(micro)

了解异步执行的运行机制

Js 的事件循环(Event Loop)机制以及实例讲解大家都知道js是单线程的脚本语言,在同一时间,只能做同一件事,为 - 掘金 (juejin.cn)

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  4. 主线程不断重复上面的第三步。

看看常见的任务

常见的宏任务包括:

  • setTimeout
  • setInterval
  • setImmediate (Node.js 独有)
  • requestAnimationFrame (浏览器独有)
  • I/O 操作
  • UI 渲染 (浏览器独有)

常见的微任务包括:

  • Promise.thenPromise.catchPromise.finally
  • process.nextTick (Node.js 独有)
  • MutationObserver (浏览器独有)
  • queueMicrotask API

记住事件循环执行顺序

在 JavaScript 中,事件循环和任务队列的执行顺序至关重要,尤其是在处理宏任务和微任务时。这里是一步步的解释,展示了宏任务和微任务是如何在事件循环中被调度和执行的:

  1. 开始执行脚本:浏览器或 Node.js 开始执行脚本文件,这是第一个宏任务。

  2. 宏任务执行:在当前宏任务执行过程中,可能会注册更多的宏任务和微任务。当前宏任务执行完成之后,不会立即执行下一个宏任务。

  3. 执行所有微任务:在当前宏任务完成后,事件循环会检查微任务队列。如果存在微任务,则执行所有微任务。这包括由 Promise.thenPromise.catchPromise.finally 生成的回调,以及 queueMicrotask() 添加的任务。微任务队列必须清空才能继续下一个宏任务。

    • 微任务中注册的微任务:如果在执行微任务期间又添加了新的微任务,这些新的微任务也会在当前所有微任务结束之前执行完毕。
  4. 渲染更新(仅在浏览器环境中):如果有必要,浏览器将处理渲染更新,如重绘和回流。这通常发生在执行完所有微任务之后,再开始下一轮宏任务之前。

  5. 下一个宏任务:完成微任务和可能的渲染更新后,事件循环将从宏任务队列中取出下一个任务开始执行,重复上述步骤。

  6. 循环重复:这个过程不断重复,直到宏任务队列为空。

理解一个具体的例子

假设我们有以下 JavaScript 代码:

console.log('1. Script start');

setTimeout(() => {
  console.log('2. setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('3. Promise 1');
}).then(() => {
  console.log('4. Promise 2');
});

console.log('5. Script end');

执行顺序如下:

  • 1. Script start:这是首个宏任务的一部分。
  • 5. Script end:仍然是首个宏任务的一部分。
  • 3. Promise 1:第一个宏任务结束后,执行所有微任务。Promise.resolve() 创建的微任务现在被执行。
  • 4. Promise 2:前一个微任务中添加的另一个微任务。
  • 2. setTimeout:第二轮宏任务开始,执行 setTimeout

这个执行顺序展示了微任务总是在当前宏任务完成后立即执行,且必须在下一个宏任务开始之前清空微任务队列。