原理
回调队列 vs 微任务队列:微任务队列和回调队列唯一的不同点在于执行优先级,其中微任务队列拥有更高的优先级,故回调队列可能会出现 starve。
因为微任务队列和回调队列的代码都是来自于回调函数,故下文在非必要情况下,全部以回调队列作为讲解。
- Event Loop
- 负责协调除程序开始运行时,即全局执行上下文(Global Execution Context)以外的整个 JS 执行过程,并决定每个回调应该在何时被执行。
- Event Loop Tick
- 当 Call Stack 为空,Event Loop 将回调队列的第一个回调放入 Call Stack 执行。
回调队列:回调队列中的回调函数既可以来自异步代码,也可以来自同步代码(比如 DOM 事件:click
、keydown
等)。
JS 引擎对时间是无感知的,因为异步根本不发生在 JS 引擎中,什么时间执行什么代码全部都由 Event Loop 决定,JS 引擎(单线程)只负责执行 Event Loop 交给它的代码而已。
简而言之,浏览器的 Web API 环境、回调队列以及 Event Loop 这些一起才实现了单线程 JS 引擎的非阻塞机制。
示例:DOM 加载图片
JavaScript
1const img = document.querySelector('.logo');
2
3// DOM 加载图片是一个异步操作
4img.src = 'logo.png';
5
6img.addEventListener('load', () => {
7 // 在图片加载完成后触发
8 p.textContent = '图片加载完成';
9});
以上代码在浏览器运行环境下的执行过程:
- 程序开始运行时,Call Stack 中加入全局执行上下文
- 在 Call Stack 顶部加入执行上下文
querySelector()
,结束并移除 - 在 Call Stack 顶部加入执行上下文
img.src
(将加载图片的行为交给 Web API),结束并移除 - 在 Call Stack 顶部加入执行上下文
addEventListener()
(将回调函数注册到 Web API),结束并移除 - 图片加载完成,浏览器 Web API 将回调放入回调队列的尾部
- Event Loop 发现 Call Stack 为空,检查微任务队列也为空,然后从回调队列中取第一个回调放入 Call Stack
- JS 引擎执行回调
代码求证
需证明以下几个结论:
setTimeout
是异步行为,其回调函数是被放入回调队列- Promise 是异步行为,其回调函数是被放入微任务队列
- 微任务队列的优先级要高于回调队列
JavaScript
1// 先注册回调队列
2setTimeout(() => console.log('0秒定时器'), 0);
3
4// 再注册微任务队列
5Promise.resolve('3秒 Promise').then(msg => {
6 // 假设在 Call Stack 中运行一段耗时代码
7 const start = Date.now();
8
9 while (Date.now() - start <= 3000) {
10 // 模拟3秒耗时操作
11 }
12
13 console.log(msg);
14});
15
16console.log('1:开始执行耗时同步代码')
17
18// 假设在 Call Stack 中正在运行一段耗时代码
19const start = Date.now();
20
21while (Date.now() - start <= 1000) {
22 // 模拟1秒耗时操作
23}
24
25console.log('2:结束执行耗时同步代码')
26
27// 1:开始执行耗时同步代码
28// 2:结束执行耗时同步代码
29// 3秒 Promise
30// 0秒定时器