# JavaScript事件循环机制
# 1. Event Loop
Event Loop(事件循环)是JavaScript处理异步操作的核心机制。它是一种程序结构,用于等待和分发事件或消息,使得JavaScript能够在单线程环境中处理并发操作。
# 1.1 为什么JavaScript是单线程
JavaScript被设计为单线程语言主要是为了简化编程模型和避免复杂的同步问题。如果JavaScript是多线程的,当一个线程修改DOM而另一个线程同时删除DOM时,会导致浏览器难以处理的冲突和不一致状态。
# 1.2 多任务处理方案
在处理多任务时,有以下几种常见方案:
- 排队:任务以队列形式进行,每次只能执行一个任务
- 新建进程:为每个任务新建一个进程
- 新建线程:由于进程耗费资源较多,而一个进程可以包含多个线程,可以通过线程来完成任务
由于JavaScript是单线程的,所有任务都在一个线程上完成,所以一旦遇到大量任务或者耗时任务,网页就会出现"假死"现象。
"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"
# 2. 微任务与宏任务详解
# 2.1 宏任务(Macro-task)
宏任务是事件循环中的主要任务类型,包括:
| 任务类型 | 浏览器环境 | Node.js环境 |
|---|---|---|
| 整体代码(script) | 是 | 是 |
| I/O操作 | 是 | 是 |
| setTimeout | 是 | 是 |
| setInterval | 是 | 是 |
| setImmediate | - | 是 |
| requestAnimationFrame | 是 | - |
| UI渲染 | 是 | - |
requestAnimationFrame是浏览器提供的API,用于在下次重绘之前执行动画或更新DOM。它会将回调函数的调用时机与浏览器的刷新频率同步,通常每秒60次(约16.67ms一次),这使得动画更加流畅。
window.requestAnimationFrame(callback);
更多信息请参阅requestAnimationFrame详解以及无线页面优化 (opens new window)
# 2.2 微任务(Micro-task)
微任务是在每个宏任务执行完毕后立即执行的任务队列,具有更高的优先级。常见的微任务包括:
| 任务类型 | 浏览器环境 | Node.js环境 |
|---|---|---|
| Promise.then/catch/finally | 是 | 是 |
| process.nextTick | - | 是 |
| queueMicrotask | 是 | 是 |
| MutationObserver | 是 | - |
MutationObserver为开发者提供了一种在DOM树发生变化时作出反应的能力。它是对旧版Mutation事件的现代化替代方案,采用异步方式批量处理DOM变化,性能更好。
更多信息请参阅深入 MutationObserver (opens new window)
# 3. 事件循环机制详解
事件循环是JavaScript处理异步操作的核心机制,其工作流程如下:
- 任务分类:同步任务直接进入主线程执行,异步任务进入事件表(Event Table)并注册回调函数
- 任务入队:当异步任务完成时,事件表会将其回调函数移入事件队列(Event Queue)
- 任务执行:主线程任务执行完毕后,会从事件队列中读取回调函数并执行
- 循环重复:上述过程不断重复,形成事件循环


在事件循环中,每进行一次完整的循环操作称为一个tick。每一次tick的任务处理模型虽然复杂,但关键步骤如下:
- 从宏任务队列中取出并执行一个宏任务(如果宏任务队列为空则等待)
- 执行该宏任务过程中产生的所有微任务,将其依次加入微任务队列
- 宏任务执行完毕后,立即按顺序执行微任务队列中的所有微任务
- 当前宏任务和所有微任务执行完毕后,开始检查并执行UI渲染
- 渲染完成后,JS线程继续执行下一个宏任务(从宏任务队列中获取)

# 4. 示例分析
以下是一些典型的事件循环示例及其执行顺序分析:
# 示例1:基础异步任务执行顺序
setTimeout(() => console.log('setTimeout-1'), 0);
const todo1 = async (params) => {
console.log('todo1-await-above');
await Promise.resolve(99);
console.log('todo1-await-under');
};
todo1();
new Promise((resolve, reject) => {
console.log('promise-1');
resolve();
}).then((data) => {
console.log('promise-then-1');
});
console.log('end');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
执行顺序分析:
setTimeout注册宏任务- 调用
todo1(),输出'todo1-await-above',遇到await将后续代码加入微任务队列 - 执行Promise构造函数,输出'promise-1',注册then微任务
- 输出'end'
- 执行微任务队列:'todo1-await-under'和'promise-then-1'
- 执行宏任务:'setTimeout-1'
# 示例2:async/await与Promise的执行顺序
const async1 = async () => {
console.log('async1 start');
await async2();
console.log('async1 end');
};
const async2 = async () => {
console.log('async2');
};
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
执行顺序分析:
- 输出'script start'
setTimeout注册宏任务- 调用
async1(),输出'async1 start',调用async2()输出'async2',await将'async1 end'加入微任务队列 - 执行Promise构造函数,输出'promise1',注册then微任务
- 输出'script end'
- 执行微任务队列:'async1 end'和'promise2'
- 执行宏任务:'setTimeout'
# 示例3:复杂嵌套的异步任务
const t1 = async () => {
console.log(1);
console.log(2);
new Promise((resolve) => {
console.log('promise3');
resolve();
}).then(() => {
console.log('promise4');
});
await new Promise((resolve) => {
console.log('b');
resolve();
}).then(() => {
console.log('t1p');
});
console.log(3);
console.log(4);
new Promise((resolve) => {
console.log('promise5');
resolve();
}).then(() => {
console.log('promise6');
});
};
setTimeout(() => {
console.log('setTimeout');
}, 0);
const t2 = async () => {
console.log(5);
console.log(6);
await Promise.resolve().then(() => console.log('t2p'));
console.log(7);
console.log(8);
};
t1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
t2();
console.log('end');
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
执行顺序分析:
- 调用
t1(),输出1,2,'promise3',注册'promise4'微任务 await将'b'后的代码加入微任务队列,注册't1p'微任务setTimeout注册宏任务- 调用
t2(),输出5,6,注册't2p'微任务 - 执行Promise构造函数,输出'promise1',注册'promise2'微任务
- 输出'end'
- 执行微任务队列:'promise4','t1p','promise2','t2p'
- 继续执行await后的代码:输出3,4,'promise5',注册'promise6'微任务
- 执行微任务:'promise6'
- 执行宏任务:'setTimeout'
# 关键要点
await之后的代码必须等await语句执行完成后(包括微任务完成),才能执行后面的,也就是说,只有运行完await语句,才把await语句后面的全部代码加入到微任务行列,所以,在遇到await promise时,必须等await promise函数执行完毕才能对await语句后面的全部代码加入到微任务中,所以,
在等待await Promise.then微任务时,
- 运行其他同步代码,
- 等到同步代码运行完,开始运行await promise.then微任务,
- await promise.then微任务完成后,把await语句后面的全部代码加入到微任务行列,
- 根据微任务队列,先进先出执行微任务
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2');
}, 0);
});
const timer1 = setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
console.log('start');
// start promise1 timer1 promise2 timer2
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5. 事件循环与浏览器渲染更新
- 在一轮event loop中多次修改同一dom,只有最后一次会进行绘制。
- 渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
- 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame
从event loop规范探究javaScript异步及浏览器更新渲染时机 (opens new window)
# 6. 参考文章
JS事件循环机制(event loop)之宏任务/微任务 (opens new window)
js中的宏任务与微任务 (opens new window)
什么是 Event Loop? (opens new window)