js事件循环机制简介
一、核心思想:为什么需要事件循环?
JavaScript 是一门单线程的编程语言。这意味着它只有一个主线程来处理所有任务。
如果所有任务(比如网络请求、定时器、用户点击)都同步执行,那么只要有一个耗时任务(如一个 5 秒的循环),页面就会卡死,无法进行任何其他操作。
为了解决这个问题,JavaScript 使用了 异步回调 的机制,而事件循环就是管理这些异步任务执行顺序的机制。
二、核心组成部分
要理解事件循环,你需要先了解三个关键部分:
-
调用栈
- 这是一个后进先出的数据结构,用于存储函数调用的执行上下文。
- 同步任务会直接被压入栈中执行。
- 当函数执行完毕,它会从栈顶被弹出。
-
Web APIs / 宿主环境
- 由浏览器(或 Node.js)提供,用于处理异步操作。
- 当遇到
setTimeout,setInterval,fetch,DOM事件监听,AJAX等异步代码时,JS 引擎会将这些任务的回调函数交给 Web APIs 去处理,然后继续执行后面的同步代码。 - Web APIs 在后台执行这些异步操作(如计时、等待网络响应)。
-
任务队列
- 当 Web APIs 完成了一个异步任务(如定时器时间到了、网络请求返回了),它会将这个任务的回调函数放入一个叫做任务队列的地方排队,等待被调用栈执行。
- 任务队列又细分为两种:
- 宏任务队列
- 微任务队列
三、宏任务 vs. 微任务
这是事件循环机制中最关键的区别!
| 类型 | 常见例子 | 优先级 |
|---|---|---|
| 宏任务 | setTimeout, setInterval, setImmediate (Node), I/O 操作, UI 渲染, 整体的 script 代码 | 低 |
| 微任务 | Promise.then(), Promise.catch(), Promise.finally(), queueMicrotask(), MutationObserver | 高 |
核心规则:在一次事件循环中,微任务队列拥有比宏任务队列更高的执行优先级。
四、事件循环的运行流程(经典模型)
你可以将事件循环想象成一个永不停止的循环,它一次又一次地执行以下步骤:
-
执行全局同步代码(这是一个宏任务):
- 从
script标签开始,将所有同步代码压入调用栈执行。 - 执行过程中,遇到的异步任务会交给 Web APIs,然后继续执行同步代码。
- 从
-
清空调用栈:
- 当调用栈中的同步任务全部执行完毕,调用栈变空。
-
执行所有微任务:
- 事件循环会立即检查微任务队列。
- 将微任务队列里的所有任务(注意:是所有,直到队列清空)依次取出,放入调用栈执行。
- 如果在执行一个微任务的过程中,又产生了新的微任务,这个新的微任务也会被加入到当前微任务队列的末尾,并在本次循环中被执行。这意味着微任务可以“插队”。
-
(可选)更新渲染:
- 浏览器可能会在这个时机进行页面的渲染更新。
-
取一个宏任务执行:
- 从宏任务队列中取出最前面一个任务(注意:是一个),放入调用栈执行。
- 这个宏任务执行完毕后,调用栈再次清空。
-
回到步骤 3:
- 再次检查微任务队列,执行所有微任务…如此循环往复。
五、经典面试题分析
让我们用这个机制来分析一段代码:
console.log('1'); // 同步代码setTimeout(() => {console.log('2'); // 宏任务回调Promise.resolve().then(() => {console.log('3'); // 微任务});
}, 0);Promise.resolve().then(() => {console.log('4'); // 微任务
});console.log('5'); // 同步代码// 输出顺序是:1, 5, 4, 2, 3
执行步骤分解:
-
执行全局同步代码(宏任务):
- 执行
console.log('1'),输出1。 - 遇到
setTimeout,将其回调函数交给 Web APIs 计时(0ms后到期),然后继续。 - 遇到
Promise.resolve().then(...),将其回调函数交给 Web APIs,Web APIs 会立即将其放入微任务队列。 - 执行
console.log('5'),输出5。 - 此时调用栈清空。
- 执行
-
执行所有微任务:
- 检查微任务队列,发现里面有
() => { console.log('4') }。 - 执行它,输出
4。 - 微任务队列清空。
- 检查微任务队列,发现里面有
-
取一个宏任务执行:
- 此时,
setTimeout的计时已到,它的回调() => { console.log('2'); ... }被放入了宏任务队列。 - 事件循环取出这个宏任务执行。
- 执行
console.log('2'),输出2。 - 执行
Promise.resolve().then(...),将其回调() => { console.log('3') }放入微任务队列。 - 这个宏任务执行完毕,调用栈清空。
- 此时,
-
再次执行所有微任务:
- 检查微任务队列,发现里面有
() => { console.log('3') }。 - 执行它,输出
3。 - 微任务队列清空。
- 检查微任务队列,发现里面有
最终输出:1, 5, 4, 2, 3
总结
- 同步任务 > 微任务 > 宏任务。
- 调用栈先执行完所有同步代码。
- 然后清空所有微任务。
- 最后再取一个宏任务执行。
- 每执行完一个宏任务,都要回头再清空一遍微任务队列。
