Published on

事件循环

Authors

事件循环是浏览器协调任务执行的机制,因为浏览器是单线程的,为了避免耗时任务卡住主线程,所以它把任务分成了 4 类

  1. 同步任务:需要立即执行的任务,会阻塞后续代码,直到其完成,可能会阻塞主线程,比如 console.log()
  2. 异步任务:不需要立即执行的任务,执行完成后需要通知主线程的,异步任务又分为宏任务和微任务。
  3. 宏任务:优先级更低的异步任务,不会阻塞主线程。包括整体 <script> 脚本的加载,setTimeout/setInterval,requestAnimationFrame,I/O 操作,DOM 事件
  4. 微任务:优先级更高的异步任务,不会阻塞主线程,但长时间运行会延迟渲染。包括 promise.then/catch/finally,async/await,MutationObserver,在当前宏任务结束后,立即执行,并清空微任务队列。

执行顺序: 整个脚本<script>本身首先作为一个宏任务执行。在这个宏任务内部,先执行所有同步代码。执行完毕后,开始清空此轮产生的微任务队列。清空后,浏览器判断是否需要重绘重排(可能进行UI渲染)。然后才开始取下一個宏任务(如setTimeout回调),并重复此过程(执行该宏任务内的同步代码->清空微任务->渲染...),形成事件循环。

应用在什么场景下?

  1. Fetch 请求(I/O 操作,宏任务)成功返回的是一个 promise.then 或者 await(微任务),确保数据拿到后可以立即处理
  2. 使用 requestAnimationFrame 优化动画(宏任务)

最佳实践是什么?

  1. 由于浏览器的单线程特性,当需要执行复杂耗时运算时,通过 setTimeout或 requestAnimationFrame(一种与渲染相关的宏任务)分片执行,也避免了线程阻塞。也可以放入 web worker 中独立处理。
  2. 除非业务必须,默认页面所有接口并发处理,使用 Promise.all,通过微任务集中处理数据,保证数据拿到后可以立即处理
  3. 现代框架的异步更新机制,依赖于微任务,将多个数据变更收集起来,在同一个事件循环的微任务阶段进行批量DOM更新,避免不必要的重复渲染,提升性能(类似于在 react 中多次 setState,react 不会在每次 setState 后立即刷新组件,更新 DOM,而是合并后,再更新一次 DOM)
  4. 由于在事件循环中,只有在微任务队列清空后,才会执行下一个宏任务。如果在微任务中递归添加微任务,就会使得宏任务无限延期,页面无法渲染和响应事件,需要避免这种操作。

拓展,一个比较复杂的示例

console.log('🔹 脚本开始执行 1')

// 宏任务:setTimeout
setTimeout(() => {
  console.log(`🟦 宏任务 setTimeout 8`)

  // 在当前宏任务中创建微任务
  Promise.resolve().then(() => {
    console.log(`🟩 宏任务产生的微任务 9`)
  })
}, 0)

new Promise((resolve) => {
  console.log(`🔹 创建Promise 2`)
  // 使用setTimeout模拟异步操作,但立即resolve
  setTimeout(() => {
    resolve(1)
  }, 0)
})
  .then((result) => {
    console.log(result, `🟩 微任务: 10`)
    // 在微任务中创建新的宏任务
    setTimeout(() => {
      console.log(`🟦 微任务产生的宏任务 14`)
    }, 50)
  })
  .finally(() => {
    console.log(`✅ 第一个微任务队列清理完毕 11`)
  })

// 创建多个Promise对象
new Promise((_, reject) => {
  // 这个console.log是同步执行的
  console.log(`🔹 创建Promise 3`)

  // 使用setTimeout模拟异步操作,但立即resolve
  setTimeout(() => {
    reject(new Error(`Error`))
  }, 0)
})
  .catch((error) => {
    console.log(error.message, `🔴 捕获到的错误: 12`)
    // 在catch中创建新的宏任务
    setTimeout(() => {
      console.log(`🟦 错误处理产生的宏任务 15`)
    }, 150)
  })
  .finally(() => {
    console.log(`✅ 第二个微任务队列清理完毕 13`)
  })

// 立即解决的Promise
Promise.resolve()
  .then(() => {
    console.log(`🟩 立即解决的Promise 5`)
    return Promise.reject('手动拒绝')
  })
  .catch((error) => {
    console.log(error, `🔴 Catch块: 6`)
  })
  .finally(() => {
    console.log(`✅ 第一个微任务队列清理完毕 7`)
  })

console.log('🔹 同步代码执行结束,开始处理异步任务 4')