一、什么是事件循环
事件循环(Event Loop)是JavaScript实现非阻塞异步编程的核心机制。由于JavaScript是单线程语言,事件循环负责协调同步任务和异步任务的执行顺序,确保主线程不被阻塞。
单线程模型的特点
- 同一时刻只能执行一个任务
- 同步任务优先执行,完成后才处理异步任务
- 耗时操作(如网络请求、定时器)会被放入任务队列等待执行
二、任务队列:宏任务与微任务
1. 微任务(Microtasks)
紧急任务,在同步任务执行完毕后立即执行,优先级高于宏任务。
微任务包括:MutationObserver、Promise.then()或catch()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收、Node独有的process.nextTick、queueMicrotask()等。
2. 宏任务(Macrotasks)
常规异步任务,在所有微任务执行完毕后才执行。
宏任务包括:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering。
同步代码和宏任务的关系
同步代码是宏任务的组成部分 ,具体表现为:
-
script 标签本身就是一个宏任务
- 整个JavaScript文件的执行以宏任务形式启动
- 宏任务内部首先执行所有同步代码
- 同步代码执行过程中可能注册新的宏任务/微任务
-
宏任务的执行包含同步代码阶段
宏任务执行流程:
┌─────────────────────┐
│ 执行宏任务中的同步代码 │
├─────────────────────┤
│ 执行所有微任务 │
└─────────────────────┘
- 每个宏任务执行时,会先运行其包含的同步代码
- 同步代码执行完毕后,才会处理微任务队列
三、事件循环的执行流程
- 执行同步代码(全局script)
- 执行所有微任务(按注册顺序)
- 执行一个宏任务
- 重复步骤2-3,形成循环
注意: 如果有多个宏任务要执行,按照先注册先执行原则,微任务也一样。
四、代码示例分析
示例1:基本执行顺序
<script>
console.log("script start"); // 同步任务
// 异步宏任务
setTimeout(() => {
console.log("setTimeout");
}, 0);
// 微任务
Promise.resolve().then(() => {
console.log('promise');
});
console.log("script end"); // 同步任务
</script>
我们按照顺序依次分析,先看第一句console.log("script start"); 它是同步任务,直接打印出来;
接着是setTimeout,它是异步宏任务,在下一轮执行;
Promise.then为微任务,先标记,不管;
console.log("script end"); 为同步任务,直接打印。
接着执行微任务,把Promise.then 的打印。
然后整体渲染一遍,进入下一轮,下一轮执行setTimeout的宏任务,打印setTimeout,再询问是否有微任务,发现没有,再渲染整个页面。
最终打印顺序依次为:
script start
script end
promise
setTimeout
示例2:多个任务队列
console.log('同步Start');
const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Second Promise');
const promise3 = new Promise(resolve => {
console.log('promise3'); // 同步执行
resolve('Third Promise');
});
promise1.then(value => console.log(value));
promise2.then(value => console.log(value));
promise3.then(value => console.log(value));
// 两个异步宏任务
setTimeout(() => {
console.log('下一把再相见');
Promise.resolve('Forth Promise').then(value => console.log(value));
}, 0);
setTimeout(() => {
console.log('下下一把再相见');
}, 0);
console.log('同步end');
输出结果为:
同步Start
promise3
同步end
First Promise
Second Promise
Third Promise
下一把再相见
Forth Promise
下下一把再相见
注意:Promise本身是同步的,所有promise3会先打印出来。而Promise.then是异步的,所以Promise.then会作为微任务,在微任务阶段处理。
接着我们对执行流程进行分解。
- 宏任务阶段
// 执行顺序:从上到下依次执行同步代码
1. console.log('同步Start'); → 输出 "同步Start"
2. 定义promise1(Promise.resolve创建的已决议Promise,.then回调进入微任务队列)
3. 定义promise2(同上,.then回调进入微任务队列)
4. 定义promise3:
- 执行Promise构造函数内的同步代码 → console.log('promise3') → 输出 "promise3"
- 调用resolve('Third Promise') → 将promise3状态改为已决议
5. 为三个Promise注册.then回调(按顺序加入微任务队列)
6. 定义第一个setTimeout → 回调函数加入宏任务队列
7. 定义第二个setTimeout → 回调函数加入宏任务队列
8. console.log('同步end'); → 输出 "同步end"
第一轮宏任务阶段输出
同步Start
promise3
同步end
2. 微任务阶段
1. promise1.then → console.log('First Promise') → 输出 "First Promise"
2. promise2.then → console.log('Second Promise') → 输出 "Second Promise"
3. promise3.then → console.log('Third Promise') → 输出 "Third Promise"
第一轮微任务输出
First Promise
Second Promise
Third Promise
3. 第二轮宏任务执行阶段
宏任务队列中的第一个setTimeout
1. console.log('下一把再相见') → 输出 "下一把再相见"
2. Promise.resolve('Forth Promise').then() → .then回调加入微任务队列
4. 第二轮微任务阶段
Promise.resolve('Forth Promise').then() → 输出"Forth Promise"
5. 第三轮宏任务阶段
宏任务队列中的第二个setTimeout
console.log('下下一把再相见'); → 输出"下下一把再相见"
五、 总结
Promise构造函数同步执行 :new Promise(executor)中的executor函数是 同步执行 的- 微任务优先级 :所有微任务会在 当前宏任务执行完毕后、下一个宏任务执行前 集中执行
- 宏任务队列顺序 :按注册顺序执行,每个宏任务执行完毕后会 先清空微任务队列 再继续
- 嵌套微任务 :宏任务中产生的新微任务会在 当前宏任务执行完毕后立即执行 ,不会等待下一个宏任务
- 任务类型判断 :
Promise.then/catch/finally→ 微任务setTimeout/setInterval→ 宏任务- 同步代码 → 立即执行