一、什么是事件循环?
JavaScript 是一门单线程语言,这意味着它一次只能执行一个任务。为了处理异步操作(如网络请求、定时器等),JavaScript 引入了事件循环机制。
事件循环的核心思想是:先执行同步代码,然后处理异步任务。异步任务又分为微任务和宏任务,它们有不同的执行优先级。
二、进程与线程基础
在深入事件循环前,我们需要了解一些基本概念:
- 进程:CPU 在运行指令和保存上下文所需要的时间。比如浏览器打开一个新标签页就是一个新进程。
- 线程:是进程中的更小单位,指执行一段指令所需的时间。一个进程可以包含多个线程,如:
- HTTP 线程
- JS 引擎线程
- 渲染线程
注意:JS 引擎线程和渲染线程是互斥的,但其他线程可以同时工作。
三、同步与异步执行
同步代码示例
let a = 1
console.log(a) // 1
这是最简单的同步代码,按顺序立即执行。
异步代码示例
setTimeout(() => {
a = 2
console.log(a) // 2 (1秒后执行)
}, 1000)
console.log(a) // 1 (立即执行)
这里 setTimeout 是异步的,不会阻塞后续代码执行。
四、微任务与宏任务
1. 微任务(Microtasks)
v8引擎会在执行时创建一个微任务队列,方便后续微任务的按序执行。
以下这些都是微任务:
Promise.thenprocess.nextTickMutationObserver
2. 宏任务(Macrotasks)
同样的,v8引擎会在执行时创建一个宏任务队列,方便后续宏任务的按序执行。
以下这些都是宏任务:
setTimeoutsetInterval- AJAX
- I/O 操作
- UI 渲染
执行顺序规则
- 执行所有同步代码(这属于宏任务),并将所有的微任务放入微任务队列,所有的宏任务放入宏任务队列。
- 依次从微任务队列中取出并执行所有微任务。
- 如有需要,渲染页面。
- 从宏任务队列中取出一个任务执行,开启下一个宏任务周期,即事件循环。
- 重复上述过程。
五、代码案例分析
案例1:基础执行
console.log(1);
new Promise((resolve) => {
console.log(2);
resolve();
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0);
});
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0);
}, 0);
console.log(7);
// 输出顺序:1 2 7 3 5 4 6
图例:
过程解析:
- 先执行同步代码,依次输出打印:
1, 2, 7,将then放入微任务队列中,将最外层的setTimeout放入宏任务队列中。 - 执行队列中唯一的微任务,先输出打印:
3,将then内部的setTimeout插入宏任务队列中,此时微任务队列为空,再从宏任务队列的队头取出一个宏任务执行。 - 执行最外层的
setTimeout宏任务,开启下一轮宏任务周期。 - 先输出打印:
5,再将最外层setTimeout嵌套的setTimeout放入宏任务队列中。 - 此时微任务队列仍为空,再从宏任务队列的队头取出一个宏任务执行,即
then内部的setTimeout,输出打印:4。 - 此后再取出宏任务队列中最后一个
setTimeout,输出打印6。
案例2:包含await行为
console.log('script start');
async function async1() {
await async2()
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve) => {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
});
console.log('script end');
// 输出顺序:
// script start
// async2 end
// promise
// script end
// async1 end
// then1
// then2
// setTimeout
图例:
关键点:
- 带有
async的函数的返回值是一个Promise对象,所以在执行时也视作同步代码。 await会将其后面的代码视为微任务,依次插入微任务队列中。v8引擎会将await这行代码视作同步代码,在宏任务中执行。
过程解析:
- 和上述案例类似,先执行同步代码,先输出
script start。然后执行async1函数,此时发现有await修饰的代码,将其后面的async1 end视作微任务,插入微任务队列中,再同步执行async2函数,输出async2 end。 - 继续向下执行同步代码,发现
setTimeout,将其插入宏任务队列中,然后依次打印Promise中的promise和末尾的script end,并将遇到的两个then视作微任务,依次插入微任务队列中。 - 以上,同步代码执行完毕。现在,从微任务队列中依次取出任务执行,即分别输出
async1 end、then1和then2。此后,微任务队列为空,从宏任务队列的队头取出一个(也仅一个)执行,输出setTimeout。
六、总结
- 避免微任务的深嵌套:
v8需要将微任务队列中的任务执行完后才开启下一个宏任务,过多的微任务有可能导致页面卡顿。 - 合理使用
setTimeout:即使延迟设为0也是宏任务,和同步代码是一个量级的。 - 理解
async/await本质:它只是Promise的语法糖,async修饰的函数返回结果就是Promise对象,而await在功能上等同于then,但await属于同步代码。 - 注意I/O性能:
I/O也属于宏任务,大量I/O操作可能影响用户体验
通过理解事件循环机制,开发者可以更好地理解异步代码的执行顺序,以此来优化性能,更能有效地避免常见的异步陷阱!