深入理解 JS —— 事件循环
什么是事件循环?
JavaScript 是单线程的语言,意味着它一次只能做一件事。但现代 Web 应用往往需要处理大量的异步操作,比如 DOM 事件处理、网络请求、定时器等。为了避免阻塞主线程,JavaScript 使用了事件循环机制来实现异步操作。
事件循环的核心是一个不断轮询的过程,用来检查是否有待处理的任务。在任务队列中有任务时,事件循环将它们取出并执行。
JavaScript 执行模型
JavaScript 的执行模型通常由以下几个部分组成:
- 调用栈(Call Stack) 调用栈是一个栈结构,用于存储当前正在执行的函数。当你调用一个函数时,它被压入栈顶,函数执行完毕后从栈顶弹出。
- 消息队列(Message Queue) 消息队列存储的是待执行的异步任务。比如,定时器回调、网络请求的响应等。
- 事件循环(Event Loop) 事件循环负责检查调用栈是否为空。如果栈为空,它就会检查消息队列中是否有待执行的任务。如果队列中有任务,事件循环就会将这些任务依次取出并压入调用栈执行。
- Web APIs(浏览器环境) Web APIs 是浏览器提供的异步 API,比如
setTimeout
、fetch
等。它们会在调用时将任务交给浏览器去处理,一旦处理完成,回调函数会被推送到消息队列中等待事件循环的执行。
事件循环的执行流程
简单来说,JavaScript 的事件循环过程可以分为以下几个步骤:
- 执行同步代码,压入调用栈。
- 当调用栈清空后,事件循环会检查消息队列中是否有待执行的任务。
- 如果有任务,事件循环会将这些任务逐个取出并执行。
具体流程
- 执行同步任务:首先,JavaScript 引擎从全局执行上下文开始,执行同步任务。同步任务会立即压入调用栈,依次执行。
- 执行异步任务:当遇到异步任务(如
setTimeout
、fetch
等),这些任务会被交给浏览器的 Web APIs 处理。当这些任务完成后,回调函数会被推送到消息队列中,等待事件循环的轮询。 - 事件循环轮询:当调用栈为空时,事件循环开始执行消息队列中的任务。它会取出队列中的任务,压入调用栈,并执行它们。
微任务与宏任务
JavaScript 中的异步任务分为两类:宏任务(Macrotask)和微任务(Microtask)。事件循环会先执行完所有宏任务,然后再执行微任务。微任务的优先级高于宏任务。
宏任务(Macrotask) | 微任务(Microtask) |
---|---|
DOM 事件(如点击、输入框变化等) | Promise.then |
I/O 操作(如文件读写) | async / await |
网络请求(Ajax) |
执行顺序
- 执行同步代码(当前宏任务)。
- 执行所有微任务(微任务队列)。
- 执行下一个宏任务,重复上述步骤。
这种机制保证了微任务比宏任务优先执行,且所有微任务都会在每个宏任务后执行完毕。
易错点
Promise
本身是一个同步的代码(只是容器),只有它后面调用的then()
方法里面的回调才是微任务1
2
3
4
5
6
7
8
9new Promise(resolve => { console.log(1); resolve(); }).then(() => { console.log(2); }); console.log(3); // 1 3 2
await
右边的表达式还是会立即执行,表达式之后的代码才是微任务,await
微任务可以转换成等价的Promise
微任务分析1
2
3
4
5
6
7
8
9
10
11
12console.log(1); async function async1() { await async2(); console.log(2); } async function async2() { console.log(3); } async1();
面试题
一.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(1);
async function async1() {
console.log(2);
await async2();
console.log(3);
await async3();
console.log(4);
}
async function async2() {
console.log(5);
}
async function async3() {
console.log(6);
}
async1();
console.log(7);
// 1 2 5 7 3 6 4
- 先执行同步代码
console.log(1)
- 然后执行异步函数
async1()
- 执行遇到第一个
await
之前的代码console.log(2)
- 执行
async2()
中的console.log(5)
- 将第一个
await
之后的代码放入微任务队列
- 执行遇到第一个
- 继续执行同步代码
console.log(7)
- 同步代码结束,执行微任务
console.log(3)
- 执行
await async3()
,完毕后将console.log(4)
放入微任务队列 - 执行
console.log(4)
二.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
console.log(1);
setTimeout(() => {
console.log(2);
new Promise(resolve => {
console.log(3);
resolve();
}).then(() => {
console.log(4);
});
console.log(5);
}, 0);
new Promise(resolve => {
console.log(6);
resolve();
}).then(() => {
console.log(7);
});
console.log(8);
// 1 6 8 7 2 3 5 4
- 先执行同步代码,输出 \(1, 6, 8\)(
Promise
是同步的,then
的回调才是微任务) setTimeout()
是宏任务,放入宏任务队列- 执行微任务
console.log(7)
- 执行宏任务队列中的 \(2, 3, 5\) ,将
then()
中的console.log(4)
放入微任务 - 执行微任务
console.log(4)
三.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
console.log(1);
async function async1() {
console.log(2);
await async2();
console.log(3);
await async3();
console.log(4);
}
async function async2() {
console.log(5);
}
async function async3() {
console.log(6);
}
console.log(7);
async1();
setTimeout(() => {
console.log(8);
}, 0);
new Promise(resolve => {
console.log(9);
resolve();
})
.then(() => {
console.log(10);
})
.then(() => {
console.log(11);
});
console.log(12);
// 1 7 2 5 9 12 3 6 10 4 11 8
- 先执行同步代码,输出 \(1,7\)
- 遇到异步函数
async1()
- 执行第一个
await
之前的console.log(2)
- 执行
async2()
,输出 \(5\) - 将第一个
await
之后的代码放入微任务队列
- 执行第一个
- 将
setTimeout()
的回调函数放入宏任务队列 - 执行同步任务
Promise
中的console.log(9)
,将第一个then()
的回调函数放入微任务队列 - 执行同步任务
console.log(12)
- 执行微任务队列中的
console.log(3)
和async3()
,将console.log(4)
放入微任务队列 - 继续执行微任务队列中的
console.log(10)
,将下一个then()
的回调函数console.log(11)
放入微任务队列 - 继续执行微任务队列中剩下的任务,输出 \(4, 11\),微任务队列清空
- 开始执行宏任务队列中的
console.log(8)
四.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
new Promise(resolve => {
console.log(1);
new Promise(resolve => {
console.log(2);
setTimeout(() => {
resolve(3);
console.log(4);
}, 0);
}).then(data => {
setTimeout(() => {
console.log(5);
}, 0);
console.log(data);
});
setTimeout(() => {
resolve(6);
console.log(7);
}, 0);
}).then(data => {
console.log(data);
setTimeout(() => {
console.log(8);
}, 0);
console.log(9);
});
// 1 2 4 3 7 6 9 5 8
- 执行同步代码
- 执行
console.log(1)
,输出 \(1\) - 执行
console.log(2)
,输出 \(2\) - 将内部
setTimeout
的回调放入宏任务队列(记为宏任务 \(1\) ) - 将外部
setTimeout
的回调放入宏任务队列(记为宏任务 \(2\) )
- 执行
- 微任务队列为空,处理宏任务队列,执行宏任务 \(1\) (内部
setTimeout
)- 将
then()
的回调放入微任务队列(记为微任务 \(1\)) - 执行
console.log(4)
,输出 \(4\)
- 将
- 宏任务 \(1\) 处理完毕,检查微任务队列,不为空,处理微任务队列,执行微任务 \(1\)
- 将
setTimeout
的回调放入宏任务队列(记为宏任务 \(3\) ) - 执行
console.log(data)
,输出 \(3\)
- 将
- 微任务队列为空,处理宏任务队列,执行宏任务 \(2\) (外部
setTimeout
)- 将
then()
的回调放入微任务队列(记为微任务 \(2\)) - 执行
console.log(7)
,输出 \(7\)
- 将
- 宏任务 \(2\) 处理完毕,检查微任务队列,不为空,处理微任务队列,执行微任务 \(2\)
- 执行
console.log(data)
,输出 \(6\) - 将
setTimeout
的回调放入宏任务队列(记为宏任务 \(4\) ) - 执行
console.log(9)
,输出 \(9\)
- 执行
- 微任务队列为空,处理宏任务队列,执行宏任务 \(3\) (微任务 \(1\) 的
setTimeout
)- 执行
console.log(5)
,输出 \(5\)
- 执行
- 微任务队列为空,处理宏任务队列,执行宏任务 \(4\) (微任务 \(2\) 的
setTimeout
)- 执行
console.log(8)
,输出 \(8\)
- 执行
附录
参考资料
深入理解 JS —— 事件循环
http://xiaowhang.github.io/archives/1540962542/