JavaScript 是一门单线程语言,这意味着同一时间只能执行一个任务。但为何在我们的页面中依然能实现异步操作、定时器、网络请求等并发效果?答案就在于 事件循环(Event Loop) 。
目录
1. JavaScript 的单线程与并发模型
JavaScript 在浏览器和 Node.js 中都是单线程执行的,这意味着所有代码都运行在一个线程上,只有一个调用栈(Call Stack)。传统上,这似乎会导致阻塞问题,但 JavaScript 的并发模型结合了异步 I/O、事件循环以及任务队列,使得它能够高效地处理并发任务。
简单来说,异步任务不会阻塞主线程,而是在任务完成后将回调函数放入队列中,等待事件循环执行。
2. 什么是事件循环
事件循环是 JavaScript 的一项机制,用于不断检查调用栈是否为空,并从任务队列中取出任务执行。
事件循环的核心思想是:
- 当调用栈为空时,将任务队列中的任务(宏任务或微任务)依次推入调用栈中执行。
- 它确保了异步任务不会干扰当前正在执行的同步代码,同时在任务完成后能够及时响应。
浏览器或 Node.js 的运行时会不断循环检查并执行任务,这就是“事件循环”。
3. 宏任务和微任务
JavaScript 任务可以分为两大类:宏任务(Macro Task) 和 微任务(Micro Task) 。
宏任务
- 定义:宏任务包含整个独立任务,比如整体的脚本代码、
setTimeout、setInterval、I/O 操作等。 - 执行时机:每一次事件循环从任务队列中取出一个宏任务执行,执行完成后再执行所有微任务。
微任务
- 定义:微任务是相对于宏任务来说更为轻量的小任务,如
Promise.then()、Promise.catch()、process.nextTick(Node.js 中)等。 - 执行时机:每次宏任务执行完毕后,事件循环会立即执行所有微任务,直到微任务队列为空后,再开始下一个宏任务。
两者关系
- 微任务的优先级高于宏任务。也就是说,在当前宏任务执行完毕后,会把所有微任务执行完再继续下一个宏任务。
- 这就解释了为什么在某些场景下,
Promise.then()的回调会先于setTimeout的回调执行。
4. 事件循环的执行顺序
一个典型的事件循环过程可以总结如下:
- 执行全局同步代码:首先执行所有同步任务,填充调用栈。
- 执行宏任务:调用栈为空后,从宏任务队列中取出第一个任务执行。
- 执行微任务:当前宏任务执行完毕后,事件循环会检查微任务队列,将所有微任务依次执行,直到微任务队列为空。
- 渲染页面(浏览器) :执行完所有微任务后,浏览器可能执行页面渲染(如果需要)。
- 继续下一个循环:返回步骤2,执行下一个宏任务。
5. 代码示例解析
下面通过一个代码示例来说明事件循环的执行顺序:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
执行过程分析
-
全局同步代码:
- 打印
script start。 setTimeout被注册为宏任务,不立即执行。Promise.resolve()创建一个已解决的 Promise,其then回调被加入微任务队列。- 打印
script end。
- 打印
-
调用栈为空:
-
当前宏任务(全局代码)结束后,事件循环检查微任务队列,开始执行所有微任务:
- 首先执行第一个
then回调,打印promise1。 - 接着,第一个
then的返回值生成新的微任务(第二个then),执行并打印promise2。
- 首先执行第一个
-
-
执行宏任务队列:
- 此时微任务队列为空,事件循环开始执行宏任务队列中的任务,即
setTimeout的回调,打印setTimeout。
- 此时微任务队列为空,事件循环开始执行宏任务队列中的任务,即
最终输出顺序:
script start
script end
promise1
promise2
setTimeout
6. 常见问题及注意事项
6.1 微任务队列可能无限执行吗?
虽然微任务队列的优先级很高,但开发者应注意不要在微任务中不断添加新的微任务,否则可能导致页面长时间卡顿甚至死循环。
6.2 与浏览器渲染的关系
在浏览器环境中,每次事件循环后,浏览器会执行页面渲染。如果微任务队列中有大量任务,会延迟页面的渲染,影响用户体验。因此,尽量将耗时的操作分散到多个事件循环中进行。
7. 最新说法
随着浏览器的复杂度急剧提升,W3C 不再使用宏队列的说法。根据 W3C 的最新解释:
- 每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列。 在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。
- 浏览器必须准备好一个微队列,微队列中的任务优先所有其他任务执行
在目前 chrome 的实现中,至少包含了下面的队列:
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 微队列:用户存放需要最快执行的任务,优先级「最高」
8. 总结
- 事件循环 是 JavaScript 处理异步任务的核心机制。
- 任务分为 宏任务 和 微任务,微任务总是在当前宏任务结束后、下一个宏任务开始前执行。
- 理解事件循环有助于编写出更高效、更健壮的异步代码,并帮助你调试一些看似神秘的执行顺序问题。