Event Loop事件循环机制

900 阅读4分钟

JavaScript在浏览器里的执行流程跟在Node.js中一样,是基于事件循环的。

事件循环:一个在JavaScript引擎等待任务、执行任务和休眠等待更多任务这几个状态之间的无穷无尽的循环。

宏任务

执行引擎通用的算法:

  1. 当有任务时从最先进入的任务开始执行
  2. 休眠到有新的任务进入,然后到第 1 步 浏览网页时,JavaScript引擎大多数时候什么也不做,只在一个脚本、处理函数或者事件被激活时运行。

一个任务到来时引擎可能正处于运行状态,那么这个任务就被入队。多个任务组成了一个队列,命名为“宏任务队列”(v8 术语),引擎按照先进先出来处理它们,然后等待更多的任务(即休眠,几乎不消耗 CPU 资源)。

宏任务举例:

  • 当外部脚本 <script src="..."> 加载进来时,任务就是执行它。
  • 当用户移动鼠标时,任务就是派发出 mousemove 事件和执行处理函数。
  • 当定时器 setTimeout 到期时,任务就是运行其回调。

当引擎忙于执行一段 script 时,还可能有用户移动鼠标产生了 mousemove 事件,setTimeout 或许也刚好到期等这些事件,这些任务组成一个队列:

当引擎处理任务时不会执行渲染,对于 DOM 的修改只有当任务执行完成才会被绘制。 如果一个任务执行时间过长,浏览器无法处理其他任务,在一定时间后就会在整个页面抛出一个如“页面未响应”的警示建议终止这个任务。这样的场景经常发生在很多复杂计算或者程序错误执行到死循环里。

使用 0 延时的 setTimeout(f)来计划一个新的宏任。 它被用来拆分一个计算耗费型任务为小片段,使浏览器可以对用户行为作出反馈和展示计算的进度。 也被用在事件处理函数中来定时执行一个行为,在当前事件被完全处理(冒泡结束)之后。

微任务

微任务仅仅由我们的代码产生。它们通常由 promises 生成:对于 .then/catch/finally 的处理函数变成了一个微任务。微任务通常"隐藏在" await 下,因为它也是另一种处理 promise 的形式。

一个宏任务结束后,先执行所有微任务队列中的任务,然后再去执行其他宏任务或渲染。微任务优先级高保证了微任务中的程序运行环境基本一致(没有鼠标位置改变,没有新的网络返回数据,等等)。 有一个特殊的函数 queueMicrotask(func),可以将 func 加入到微任务队列来执行。如果我们想要异步执行(在当前代码之后)一个函数,但是要在修改被渲染或者新的事件被处理之前,我们可以用 queueMicrotask 来定时执行。

setTimeout(() => alert("timeout"));
Promise.resolve()
  .then(() => alert("promise"));
alert("code");
// code -> promise -> timeout

更具体的事件循环的算法:

  1. 从宏任务队列出列并执行最前面的任务(比如“script”)。
  2. 执行所有的微任务:
  3. 当微任务队列非空时:出列并运行最前面的微任务。
  4. 如有需要执行渲染。
  5. 如果宏任务队列为空,休眠直到一个宏任务出现。
  6. 到步骤 1 中。

Promise与微任务

Promise 的处理程序(handlers).then、.catch 和 .finally 都是异步的。 异步任务需要适当的管理。为此,JavaScript 标准规定了一个内部队列 PromiseJobs —— “微任务队列”(Microtasks queue)(v8 术语)。 这个队列先进先出,只有引擎中没有其他任务运行时才会启动任务队列的执行。 当一个 promise 准备就绪时,它的 .then/catch/finally 处理程序就被放入队列中。等到当前代码执行完并且之前排好队的处理程序都完成时,JavaScript引擎会从队列中获取这些任务并执行。 即便一个 promise 立即被 resolve,.then、.catch 和 .finally 之后的代码也会先执行。 如果要确保一段代码在 .then/catch/finally 之后被执行,最好将它添加到 .then 的链式调用中。

let promise = Promise.resolve();

promise.then(() => alert("promise done"));

alert("code finished"); // 该警告框会首先弹出
未处理的 rejection

指在 microtask 队列结束时未处理的 promise 错误。 microtask队列完成时,引擎会检查promise,如果其中任何一个出现rejected状态,就会触发unhandledrejection事件。 但如果在setTimeout里进行catch,unhandledrejection会先触发,然后catch才执行,所以catch没有发挥作用。

let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')));
window.addEventListener('unhandledrejection', event => alert(event.reason));
// Promise Failed! -> caught