事件循环

为什么

JS 的一大特点就是单线程,也就是说,同一时间只能做一件事。那为什么要设计成单线程呢,多线程效率不是更高吗?

有这样一个场景:假定 JS 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,JS 从诞生就是单线程。但是单线程就导致有很多任务需要排队,只有一个任务执行完才能执行后一个任务。如果某个执行时间太长,就容易造成阻塞;为了解决这一问题,JS 引入了事件循环机制。

任务队列

首先我们需要明白以下几件事情:

  • JS 分为同步任务和异步任务。
  • 同步任务都在主线程上执行,形成一个执行栈。
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时 JS 引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

根据规范,事件循环是通过任务队列的机制来进行协调的。 一个 Event Loop 中,可以有一个或者多个任务队列 (task queue),一个任务队列便是一系列有序任务 (task) 的集合。 每个任务都有一个任务源 (task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。

event_loop_01

宏微任务

  • 浏览器的任务队列
    • 主任务队列:存储的都是同步任务
    • 等待任务队列:存储的都是异步任务
      • 宏任务队列
        • script 整体
        • setTimeout
        • setInterval
        • setImmediate (nodeJS)
        • requestAnimationFrame (html5)
      • 微任务队列
        • await
        • Promise.then
        • process.nextTick (nodeJS)
        • MutationObserver (html5)

运行机制

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 微任务都执行完毕后,开始下一个宏任务(从事件队列中获取)

event_loop_03

参考

Last Updated:
Contributors: Vsnoy