JS 中的事件循环机制
引言
JavaScript 是一种单线程的语言,这意味着它在同一时间只能执行一个任务。然而,在现代 Web 开发中,我们需要处理许多异步操作,如网络请求、定时器等。为了管理这些异步操作并保持代码的执行效率,JavaScript 引入了事件循环机制,以及区分宏任务和微任务的概念。本文将深入探讨这些概念,并通过示例代码帮助理解它们是如何工作的。
核心知识点
1. 同步与异步
JavaScript 的执行模型分为同步任务和异步任务。同步和异步是计算机编程中两种不同的执行模式。同步任务会直接在主线程上执行,而异步任务则会被放入一个队列中等待执行。
-
同步:在同步操作中,程序的执行会阻塞或者等待一个任务完成之后再继续向下执行。这意味着调用者必须等待直到被调用的任务完成才能继续下一步操作。这种模式简单直接,但在处理耗时操作时会导致性能瓶颈。
-
异步:在异步操作中,程序不需要等待一个任务完成就可以继续执行其他任务。一旦启动了一个异步操作,调用者可以立即进行下一个操作,而不会被阻塞。当异步操作完成时,通常会通过回调函数、事件通知或者 Promise(对于支持的语言如 JavaScript)来告知结果。
2. 宏任务与微任务
-
宏任务:包括
全局执行上下文、setTimeout、setInterval等,这些任务会在当前的执行栈清空之后执行。 -
微任务:包括
Promise.then()、process.nextTick()、MutationObserver()、await 后面的代码等,这些任务会在当前宏任务的执行栈清空之后立即执行,但在此之前所有微任务都会先执行完毕。
注意:在微任务队列中,process.nextTick()的优先级最高,最先会从微任务队列中拿出来执行。
3. 事件循环机制
JavaScript 的执行引擎会不断从任务队列中取出任务来执行,这个过程被称为事件循环。每次事件循环包括以下几个阶段:
- 执行栈:执行同步任务,直到执行栈为空。
- 微任务队列:执行所有的微任务,直到队列为空。
- 宏任务队列:如果还有宏任务,则开始下一个宏任务的执行,从而开启新的事件循环。
4. 事件循环的执行流程
- 执行同步代码:首先执行所有同步代码。
- 检查异步任务:同步代码执行完毕后,检查是否有异步任务需要执行。
- 执行所有的微任务:在执行下一个宏任务之前,执行所有等待中的微任务。
- 渲染页面:如果需要,执行用户界面的渲染。
- 执行宏任务:执行等待中的宏任务,从而开始下一轮事件循环。
5. 示例代码分析
下面是一个具体的例子来说明上述概念:
console.log('script start');
async function async1() {
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1();
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve();
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
});
console.log('script end');
(1)输出结果预测:
script startasync2 endpromisescript endasync1 endthen1then2setTimeout
(2)解析:
-
同步代码:
console.log('script start');是第一个被执行的同步代码,输出script start。
-
异步代码:
async1()被调用,其中await async2();会等待async2()执行完毕。async2()打印出async2 end。async1()继续执行,打印出async1 end。
-
微任务:
Promise.resolve().then(() => {...})是一个微任务,它会在当前宏任务结束之后立即执行,因此then1和then2会紧接着输出。
-
宏任务:
console.log('script end');是同步代码的一部分,所以接下来输出script end。setTimeout是一个宏任务,尽管它的延时为0,但需要等待当前宏任务完成及所有微任务执行完毕后才会执行。因此最后输出setTimeout。
(4)具体步骤:
-
执行同步代码:
- 输出
script start。
- 输出
-
执行
async1():- 这个函数被调用,但由于
await的存在,它会等待async2()的执行。 async2()被调用,输出async2 end。
- 这个函数被调用,但由于
-
执行
Promise构造函数:- 创建一个新的
Promise对象并在构造函数中立即执行,输出promise。 Promise对象的.then()方法被注册,但不会立即执行。
- 创建一个新的
-
执行同步代码:
- 输出
script end。
- 输出
-
执行微任务:
Promise的.then()方法作为微任务开始执行,输出then1和then2。
-
执行宏任务:
-
setTimeout作为宏任务执行,输出setTimeout。
-
总结
JavaScript 的事件循环机制是其异步处理的核心。理解宏任务和微任务的区别对于编写高效、可靠的代码至关重要。通过本文,我们不仅深入了解了 JavaScript 中的事件循环机制,还学习了如何区分宏任务和微任务,并通过实际代码示例加深了理解。通过本篇文章的学习,希望你能够更加熟练地掌握 JavaScript 的异步编程技巧。