事件循环机制
事件循环机制(Event Loop)是 JavaScript 中处理异步操作的核心机制。由于 JavaScript 是单线程语言,它需要通过事件循环机制来协调同步任务和异步任务的执行,使得程序能够以非阻塞的方式处理 I/O 操作、定时器、用户交互等异步事件。
1. JavaScript 执行模型
JavaScript 的执行分为两种任务:
- 同步任务:直接在主线程上排队执行,只有当前任务完成后,才能执行下一个任务。
- 异步任务:不会立即执行,异步操作将注册回调,当异步任务完成后,回调函数会被放入任务队列中,等待主线程空闲时再执行。
2. 事件循环的核心概念
事件循环的工作流程可以概括为以下步骤:
- 执行所有同步任务:首先执行全局代码中的同步任务。
- 检查异步任务队列:主线程空闲后,检查是否有异步任务完成并将其回调函数放入任务队列。
- 执行任务队列中的回调:按照顺序从任务队列中取出回调函数,并将其执行。
- 重复步骤 2 和 3:不断重复此过程,以确保事件能够持续处理。
3. 任务队列类型
任务队列(任务队列或消息队列)分为两类:
- 宏任务队列(Macro Task Queue):包括整体代码(script)、
setTimeout
、setInterval
、I/O
操作、事件监听等。 - 微任务队列(Micro Task Queue):包括
Promise.then()
、process.nextTick()
、MutationObserver
等。
事件循环在每一轮中都会优先执行所有的微任务队列,然后再执行一个宏任务。
4. 事件循环的执行顺序
事件循环的执行顺序非常关键,遵循以下流程:
- 同步代码先执行:主线程上所有同步任务先按顺序执行。
- 执行所有的微任务:当同步代码执行完后,立即执行微任务队列中的所有微任务,直到微任务队列清空。
- 执行一个宏任务:接着执行宏任务队列中的第一个宏任务。
- 循环往复:每次宏任务执行完后,都会进入微任务队列,优先执行微任务,再回到宏任务,形成循环。
5. 宏任务与微任务的示例
下面通过示例展示宏任务和微任务的执行顺序:
1 |
|
执行顺序解析:
- 首先执行同步任务,依次打印
'1'
和'4'
。 - 将
setTimeout
回调放入宏任务队列,并将Promise.then
回调放入微任务队列。 - 执行完同步代码后,进入微任务阶段,执行微任务
Promise.then
回调,打印'3'
。 - 最后进入宏任务阶段,执行
setTimeout
回调,打印'2'
。
最终输出顺序为:
1 |
|
6. 示例详解:宏任务与微任务交替
为了更好地理解事件循环,可以再来看一个更复杂的例子:
1 |
|
执行顺序解析:
- 同步任务依次执行,打印
'A'
和'F'
。 - 宏任务
setTimeout
的回调进入宏任务队列,而Promise.then
的回调进入微任务队列。 - 同步任务执行完毕后,进入微任务阶段,执行所有微任务,依次打印
'C'
和'E'
。 - 微任务执行完毕后,进入宏任务阶段,执行宏任务队列中的回调,依次打印
'B'
和'D'
。
最终输出顺序为:
1 |
|
7. 浏览器中的事件循环
在浏览器环境中,事件循环与 UI 渲染之间也存在密切联系。每次宏任务执行结束后,浏览器有机会去渲染页面,所以页面的更新通常会在宏任务结束后执行,而不会在微任务中间插入。
8. Node.js 中的事件循环
在 Node.js 中,事件循环的机制与浏览器类似,但有一些区别。Node.js 的事件循环中有多个阶段,每个阶段都有不同类型的任务处理。例如:
- timers 阶段:处理
setTimeout
和setInterval
。 - I/O callbacks 阶段:处理系统操作的回调。
- poll 阶段:检索新的 I/O 事件。
- check 阶段:执行
setImmediate()
回调。 - close 阶段:关闭回调处理。
每个阶段结束后,都会执行微任务队列(Promise.then()
和 process.nextTick()
)。
9. 常见面试题分析
问题 1:
1 |
|
执行顺序:
- 同步代码先执行,打印
'start'
和'end'
。 setTimeout
回调进入宏任务队列。Promise.then()
回调进入微任务队列。- 执行微任务队列,打印
'promise1'
和'promise2'
。 - 最后执行宏任务
setTimeout
,打印'setTimeout'
。
输出:
1 |
|
总结:
- 事件循环是 JavaScript 中处理异步任务的核心机制,它通过任务队列和微任务队列的配合实现非阻塞的异步操作。
- 在每个事件循环周期中,先执行同步代码,然后执行微任务队列,最后执行宏任务队列中的一个宏任务。
- 微任务(如
Promise.then()
)优先于宏任务(如setTimeout()
)执行,这导致了在实际编写代码时,微任务通常会比宏任务先执行。
小测试
1 |
|
让我们逐行分析这段代码的输出顺序:
console.log('script start');
:立即输出'script start'
。setTimeout(..., 0);
:这个回调会被放入宏任务队列,等待执行。new Promise(...);
:- 立即执行,输出
'promise'
。 - 调用
rej()
,进入.catch()
。
- 立即执行,输出
.catch(...)
:处理拒绝状态,返回1
,但没有输出(只是返回值)。- 接下来的
.then(...)
:这个then
会在微任务队列中被添加。 console.log('script end');
:立即输出'script end'
。
接下来,微任务队列会先执行:
7. 输出 'promise2'
:因为上一个 then
返回了一个值。
最后,宏任务队列中的 setTimeout
回调执行:
8. 输出 'setTimeout'
。
所以,最终输出顺序为:
1 |
|
事件循环机制
https://garlandqian.github.io/2024/09/24/Interview/js/事件循环机制/