此篇文章写的浏览器端的事件循环( EventLoop),Node 端会有所不同。
1 事件循环(EventLoop)
事件循环是 JS 主线程处理任务的过程,包含如下元素:
- 堆
- 调用栈(也叫执行栈)
- 由一系列函数调用组成的栈
- 消息队列(也叫任务队列、宏任务队列、callback queue)
- 由待处理的宏任务组成的队列,如 被触发的 click 事件、到达定时时间的任务、已完成的网络请求 等
- 微队列
- 由待处理的微任务组成,如 Promise 的回调,MutationObserver 回调等
1.1 事件循环视图
上图展示了事件循环的过程:
- 当前正在执行的函数被压入执行栈(stack)中
- 函数执行过程中调用 Promise、MutationObserver 等 WebAPI 产生的异步任务需要执行时,会被插入微任务队列(microtask queue) 中。
- 函数执行过程中调用 Ajax、setTimeout 等 WebAPI 产生的异步任务需要执行时,会被插入宏任务队列(task queue) 中。
- 执行栈中的所有函数执行完成后(即栈空后),JS 主线程会首先从 microtask queue 中取出一个微任务进行执行,执行微任务就是把微任务对应的回调函数压入执行栈中进行执行,即回到步骤 1 。微任务队列为空时才会执行第 3 步。
- 微任务队列为空时,JS 主线程再从 task queue 中取出一个宏任务进行执行,执行宏任务也是将宏任务对应的回调函数压入执行栈中进行执行,即回到了步骤 1。
2 堆
对象会被分配到堆
内存中。
3 调用栈(执行栈)
函数被执行时,会产生对应的执行上下文(Execute Context,EC,也叫执行环境),EC 会被压入调用栈
执行。
当有多个函数被嵌套调用时,会有多个 EC 被压入调用栈
。
1 | function foo(b) { |
上面代码执行过程:
- 主代码的 EC 被压入栈中执行。
- 当调用 bar 函数时,bar 的 EC 被压入栈中执行。
- 当 bar 调用 foo 时,foo 的 EC 被压入栈中执行。
- 当 foo 执行完成时,foo 的 EC 被弹出栈。
- 当 bar 执行完成时,bar 的 EC 也被弹出栈。
- 主代码块执行完成,其 EC 也被弹出栈,此时栈被清空了。
4 消息队列
消息队列(也叫任务队列、宏任务队列、callback queue、task queue)由待处理的消息组成,事件循环会依次处理消息队列中的消息。
消息被处理时会被移出消息队列,并作为输入参数来调用与之关联的回调,回调被执行会被压入调用栈中执行。
4.1 宏任务
宏任务(MacroTask,也叫 Task),由下面类型的代码形成,并被加入到宏任务队列:
被触发的事件回调,如 onClick、onLoad
setTimeout / setInterval
5 微任务队列
微任务队列由微任务组成,事件循环会依次处理微任务队列中的微任务。
5.1 微任务
微任务(MicroTask,也叫 Jobs),下面类型的代码属于微任务,并被加入微任务队列:
Promise
MutationObserver
监控 DOM 树的更改
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// 选择需要观察变动的节点
const targetNode = document.getElementById('some-id');
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
// 之后,可停止观察
observer.disconnect();
queueMicrotask
- ```js
queueMicrotask(() => {
/* 微任务中将运行的代码 */
});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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
- 使用场景
- 代码执行结果的一致性
- 串行执行任务
# 6 执行顺序
JS 主线程执行顺序:
1. 当前执行栈中的代码
2. 当前执行栈所产生的 microtask(直到微任务队列中任务全部执行完成,即为空时才进行下一步)
3. 渲染绘制
4. 下个 task
示例:
```js
setTimeout(function() {
console.log(5)
setTimeout(function(){
console.log(9);
})
Promise.resolve().then(function() {
console.log(6);
})
}, 0);
new Promise(function executor(resolve) {
console.log(1);
setTimeout(function(){
console.log(7);
});
Promise.resolve().then(function() {
console.log(3);
Promise.resolve().then(function() {
console.log(4);
setTimeout(function(){
console.log(8);
});
})
})
}).then(function() {
console.log(4);
});
console.log(2);
//输出顺序为:
1
2
3
4
undefined
5
6
7
8
9
- ```js
参考资料:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop