JavaScript 的事件循环(Event Loop)是 JavaScript 运行时环境(如浏览器和 Node.js)中的一个核心概念,它负责处理异步事件和回调。为了更好地理解事件循环,我们需要深入探讨微任务(microtasks)和宏任务(macrotasks)的概念。
事件循环(Event Loop)
事件循环(Event Loop)是 JavaScript 的核心机制,用于处理异步操作和确保代码按正确的顺序执行。简单来说,事件循环就是 JavaScript 的“调度员”,它决定了代码的执行顺序,确保异步操作能够在适当的时候被处理。
JavaScript 是单线程的,这意味着它一次只能执行一个任务。但是,JavaScript 需要处理异步操作,如网络请求、文件读取等,这些操作可能会花费很长时间才能完成。事件循环允许 JavaScript 在等待这些异步操作完成时,继续执行其他任务。
宏任务(Macrotasks)
宏任务是指那些被放在每次事件循环的宏任务队列中执行的任务。每次事件循环只会从宏任务队列中取出一个任务来执行。常见的宏任务包括:
script(整体代码)setTimeoutsetIntervalsetImmediate(Node.js 特有)- I/O
- UI 渲染
宏任务之间会有一次渲染(在浏览器环境下),这是浏览器为了确保页面能够响应用户的操作而进行的。
微任务(Microtasks)
微任务与宏任务类似,但它们在每个宏任务之后、渲染之前执行。微任务队列中的任务会按照它们被添加到队列的顺序执行,直到队列为空。常见的微任务包括:
Promise.then、Promise.catch、Promise.finallyMutationObserver的回调process.nextTick(Node.js 特有)async awaitawait后的代码
事件循环的工作流程
-
执行栈:想象有一个桌子,上面放着你要做的工作任务。你把任务按顺序放在桌子上,逐个完成它们。这就是 JavaScript 的执行栈,你的同步代码会逐个被放上去执行。
-
任务队列:在桌子旁边有一个待办事项列表,包含需要处理的异步任务。这个列表分为两部分:
- 宏任务队列:较大的任务,比如设置的定时器或网络请求的回调。
- 微任务队列:较小的任务,优先级更高,比如
Promise的回调。
-
事件循环:这是负责查看待办事项列表的人员。它的工作是这样的:
- 首先,完成桌子上的所有任务(执行栈中的同步代码)。
- 然后,查看微任务队列,处理所有的微任务。
- 接着,从宏任务队列中取出下一个任务,放到桌子上执行。
- 重复这个过程,直到所有任务都处理完。
步骤概述
-
执行同步代码:JavaScript 首先执行所有同步代码,这些代码被放入执行栈中,逐个完成。
-
处理微任务:当执行栈中的同步代码执行完毕后,事件循环会查看微任务队列,处理其中的所有任务。微任务包括
Promise的回调。 -
处理宏任务:处理完所有微任务后,事件循环会从宏任务队列中取出一个任务(如定时器的回调)并执行。
-
渲染更新:在处理宏任务之间,浏览器可能会进行渲染更新,以确保页面显示正确。
-
重复:事件循环会继续重复以上步骤,直到所有任务处理完成。
示例
javascript复制代码
console.log('script start'); // 这是宏任务(script)中的同步代码
setTimeout(function() {
console.log('setTimeout'); // 这是另一个宏任务,被添加到宏任务队列中
}, 0);
Promise.resolve().then(function() {
console.log('promise1'); // 这是一个微任务,被添加到当前宏任务(script)的微任务队列中
}).then(function() {
console.log('promise2'); // 这是另一个微任务,也添加到当前宏任务(script)的微任务队列中
});
console.log('script end'); // 这是宏任务(script)中的另一个同步代码
// 执行顺序解析:
// 1. 执行宏任务(script)中的同步代码:
// - 打印 "script start"
// - 遇到 setTimeout,将其回调函数添加到宏任务队列中(但尚未执行)
// - 执行 Promise.resolve().then(...) 链,将两个回调函数作为微任务添加到当前宏任务(script)的微任务队列中
// - 打印 "script end"
// 2. 宏任务(script)的同步代码执行完毕后,检查并清空微任务队列:
// - 执行并打印 "promise1"
// - 执行并打印 "promise2"
// 3. 微任务队列清空后,事件循环继续检查宏任务队列,但此时没有新的宏任务需要立即执行(注意:setTimeout 的回调虽然已被添加到队列,但它不会立即执行,因为事件循环会先处理完所有微任务)
// 如果是在浏览器环境中,并且没有其他宏任务需要执行,此时浏览器可能会进行渲染操作(但在这个简单的脚本示例中,我们不会看到渲染效果)。
// 4. 最终,当事件循环再次检查宏任务队列时,它会发现 setTimeout 的回调,并将其作为下一个宏任务执行,打印 "setTimeout"
所以,尽管 `setTimeout` 的回调被添加到宏任务队列中的时间早于 `Promise.then` 的回调,但由于 `Promise.then` 的回调是作为微任务在当前宏任务(script)中立即被添加到微任务队列中的,因此它们会在宏任务(script)的同步代码执行完毕后、且在下一个宏任务(如 `setTimeout` 的回调)之前被执行。
在这个例子中,setTimeout 是一个宏任务,而 Promise.then 产生的回调是微任务。因此,尽管 setTimeout 被设置为立即执行(0 毫秒后),它仍然会在所有微任务之后执行。