🍩 JavaScript 事件循环
浏览器中 JavaScript 的执行流程和 Node.js 中的流程都是基于 事件循环 的。理解事件循环的工作方式对于代码优化很重要,有时对于正确的架构也很重要。
# 事件循环
是一个在 JavaScript 引擎 等待任务,执行任务 和 进入休眠状态等待更多任务 这几个状态之间转换的无限循环。
引擎的一般算法:
- 当有任务时:从最先进入的任务开始执行。
- 休眠直到出现任务,然后转到第 1 步。
设置任务 —— 引擎处理它们 —— 然后等待更多任务(即休眠,几乎不消耗 CPU 资源)。
一般浏览网页就是这种形式,JavaScript 引擎大多数时候不执行任何操作,它仅在脚本 / 处理程序 / 事件激活时执行。
浏览器任务一般有:
- 当外部脚本
<script src="...">
加载完成时,任务就是执行它;- 当用户移动鼠标时,任务就是派生出
mousemove
事件和执行处理程序;- 当安排的(scheduled)
setTimeout
时间到达时,任务就是执行其回调等。
一个任务到来时,引擎可能正处于繁忙状态,那么这个任务就会被排入队列。多个任务组成了一个队列,为 宏任务队列 (V8 引擎)。
🌰 例子:
当引擎正在忙于执行一段 script
时,用户可能会移动鼠标而产生 mousemove
事件, setTimeout
或许也刚好到期,以及其他任务,这些任务组成了一个队列。
这个队列的任务基于 「先进先出」 的原则执行。当浏览器引擎执行完 script
后,它会处理 mousemove
事件,然后处理 setTimeout
处理程序,依此类推。
- 引擎执行任务时永远不会进行渲染(render)。如果任务执行需要很长一段时间也没关系。仅在任务完成后才会绘制对 DOM 的更改。
- 如果一项任务执行花费的时间过长,浏览器将无法执行其他任务,例如处理用户事件。因此,在一定时间后,浏览器会抛出一个如 “页面未响应” 之类的警报,建议终止这个任务。这种情况常发生在有大量复杂的计算或导致死循环的程序错误时。
# 宏任务 / 微任务
微任务仅来自于代码。它们通常是由 promise 创建的:对 .then/catch/finally
处理程序的执行会成为微任务。微任务也被用于 await
的「幕后」,因为它是 promise 处理的另一种形式。
特殊的函数 queueMicrotask(func)
,它对 func
进行排队,以在微任务队列中执行。
🌰 例子:
setTimeout(() => alert("timeout"));
Promise.resolve()
.then(() => alert("promise"));
alert("code");
2
3
4
5
6
按照上面的代码,执行顺序应该为:
code
显示;promise
第二个出现。因为then
会通过微任务队列,并在当前代码之后执行。timeout
最后显示,因为它是一个宏任务。
按照更加详细的事件循环:(首先是脚本,然后是微任务,渲染等)
- 微任务 会在执行任何其他事件处理,或渲染,或执行任何其他宏任务 之前完成。(这确保了微任务之间的应用程序环境基本相同)
- 如果想异步执行(在当前代码之后)一个函数,但是要在更改被渲染或新事件被处理之前执行,那么可以使用
queueMicrotask
来对其进行安排。
🌰 例子 / 计数进度条的实现,使用了 queueMicrotask
安排:
<div id="progress"></div>
<script>
let i = 0;
function count() {
// 做繁重的任务的一部分 (*)
do {
i++;
progress.innerHTML = i;
} while (i % 1e3 != 0);
if (i < 1e6) {
queueMicrotask(count);
}
}
count();
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 总结
详细的 事件循环 算法:
从 宏任务 队列中出队并执行最早的任务。
执行所有的微任务:
- 当微任务队列非空时:
- 出队并执行最早的微任务。
- 当微任务队列非空时:
如果有变更,则将变更渲染出来。
如果宏任务队列为空,则休眠直到出现宏任务。
转到步骤 1。
安排新的宏任务:
- 使用零延迟的
setTimeout(f)
。
它可被用于将繁重的计算任务拆分成多个部分,以使浏览器能够对用户事件作出反应,并在任务的各部分之间显示任务进度。
此外,也被用于在事件处理程序中,将一个行为安排在事件被完全处理(冒泡完成)后。
- 使用零延迟的
安排新的为任务:
- 使用
queueMicrotask(f)
; - promise 处理程序也会通过微任务队列。
在微任务之间没有 UI 或网络事件的处理:它们一个立即接一个地执行。所以,可以使用
queueMicrotask
来在保持环境状态一致的情况下,异步地执行一个函数。- 使用
提示
Web Workers 的使用,当有 不应该阻塞事件循环的耗时长的繁重计算任务,这时另一个并行线程运行代码的方式:
- Web Workers 可以与主线程交换消息,但是它们具有自己的变量和事件循环。
- Web Workers 没有访问 DOM 的权限,因此,它们对于同时使用多个 CPU 内核的计算非常有用。