前言
在日常开发中,你是否遇到过这样的困惑:为什么有些代码的执行顺序和预期不一致?为什么Promise会比setTimeout先执行?这些问题的答案都指向JavaScript的核心机制------事件循环(Event Loop)。
事件循环是JavaScript执行机制的心脏,也是理解异步编程的关键。今天,我们通过实际代码案例,深入剖析这个看似复杂但实则优雅的机制。
JavaScript单线程的本质
JavaScript作为单线程语言,同一时刻只能处理一件事。这意味着:
- 同步任务需要尽快执行完成
- 页面渲染(重绘重排)必须及时响应
- 用户交互要优先处理
- 耗时性任务不能阻塞主线程
那么,耗时性任务如何处理呢?这就涉及到异步任务的概念。
宏任务与微任务的区别
宏任务(Macro Task)
每个script脚本就是一个宏任务的开始。常见的宏任务包括:
- setTimeout/setInterval
- fetch/ajax请求
- DOM事件监听器
- script标签执行
微任务(Micro Task)
微任务是紧急的、优先的,是同步任务执行完后的一个补充。包括:
- Promise.then()、Promise.catch()、Promise.finally()
- MutationObserver(DOM变化监听)
- queueMicrotask
- process.nextTick()(Node.js环境)
⚠️ 重要提醒:Promise本身不是微任务!
new Promise(executor)
中的executor函数是同步执行的- 只有Promise的
.then()
、.catch()
、.finally()
回调才是微任务 Promise.resolve()
和Promise.reject()
创建的是已resolved/rejected的Promise,但仍需要.then()
才产生微任务
经典案例分析
案例一:基础执行顺序
javascript
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('script end');
执行结果:
arduino
script start
script end
promise
setTimeout
详细执行步骤解析:
第1步:同步任务执行
- 执行
console.log('script start')
,输出:script start
- 遇到
setTimeout()
,这是一个宏任务,将回调函数放入宏任务队列等待 - 遇到
Promise.resolve()
,创建一个已resolved的Promise对象(这本身是同步的) - 调用
.then()
,将then回调放入微任务队列(这里才产生微任务) - 执行
console.log('script end')
,输出:script end
第2步:微任务队列检查
- 同步任务执行完毕,检查微任务队列
- 发现Promise.then()回调,立即执行,输出:
promise
- 微任务队列清空
第3步:宏任务队列执行
- 微任务队列为空,从宏任务队列取出setTimeout回调执行
- 输出:
setTimeout
关键理解:
- 同步任务 > 微任务 > 宏任务
- 微任务队列必须完全清空后才会执行宏任务
- Promise.resolve()是同步的,只有.then()才产生微任务
案例二:复杂的嵌套异步
javascript
console.log('start');
Promise.resolve().then(() => {
console.log('promise3');
setTimeout(() => {
console.log('timeout3');
}, 0);
});
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
console.log('end');
执行结果:
sql
start
end
promise3
timeout1
promise1
timeout2
promise2
timeout3
详细执行步骤解析:
第1步:同步任务执行
- 执行
console.log('start')
,输出:start
- 遇到
Promise.resolve().then()
,将回调放入微任务队列 - 遇到第一个
setTimeout()
,将回调放入宏任务队列(记为timeout1) - 遇到第二个
setTimeout()
,将回调放入宏任务队列(记为timeout2) - 执行
console.log('end')
,输出:end
第2步:微任务队列执行
- 同步任务完成,检查微任务队列
- 执行Promise回调,输出:
promise3
- 在Promise回调中遇到
setTimeout()
,将新的回调放入宏任务队列(记为timeout3) - 微任务队列清空
第3步:第一个宏任务执行
- 从宏任务队列取出timeout1执行
- 输出:
timeout1
- 在timeout1中遇到
Promise.resolve().then()
,将回调放入微任务队列 - 检查微任务队列,执行Promise回调,输出:
promise1
第4步:第二个宏任务执行
- 微任务队列为空,取出timeout2执行
- 输出:
timeout2
- 在timeout2中遇到
Promise.resolve().then()
,将回调放入微任务队列 - 检查微任务队列,执行Promise回调,输出:
promise2
第5步:第三个宏任务执行
- 微任务队列为空,取出timeout3执行
- 输出:
timeout3
关键理解:
- 每个宏任务执行完后,都会清空微任务队列
- 新产生的宏任务会按顺序排队
- 宏任务中产生的微任务会在下一个宏任务执行前完成
案例三:Node.js环境的特殊性
javascript
console.log('Start');
process.nextTick(() => {
console.log('Process Next Tick');
});
Promise.resolve().then(() => {
console.log('Promise Resolved');
});
setTimeout(() => {
console.log('六百六十六');
Promise.resolve().then(() => {
console.log('inner Promise');
});
}, 0);
console.log('end');
执行结果:
sql
Start
end
Process Next Tick
Promise Resolved
六百六十六
inner Promise
详细执行步骤解析:
第1步:同步任务执行
- 执行
console.log('Start')
,输出:Start
- 遇到
process.nextTick()
,将回调放入Node.js特有的nextTick队列 - 遇到
Promise.resolve().then()
,将回调放入微任务队列 - 遇到
setTimeout()
,将回调放入宏任务队列 - 执行
console.log('end')
,输出:end
第2步:Node.js微任务执行
- 同步任务完成,先检查nextTick队列(优先级最高)
- 执行process.nextTick回调,输出:
Process Next Tick
- 然后检查Promise微任务队列
- 执行Promise回调,输出:
Promise Resolved
- 所有微任务清空
第3步:宏任务执行
- 从宏任务队列取出setTimeout回调执行
- 输出:
六百六十六
- 在setTimeout中遇到
Promise.resolve().then()
,将回调放入微任务队列 - 检查微任务队列,执行Promise回调,输出:
inner Promise
Node.js特殊性:
process.nextTick()
优先级高于Promise.then()
- nextTick队列会在Promise微任务队列之前执行
- 这是Node.js特有的微任务处理机制
DOM操作中的微任务
MutationObserver的应用
javascript
// HTML文档第一个BFC(块级格式化上下文)
const target = document.createElement('div');
document.body.appendChild(target);
const observer = new MutationObserver(() => {
console.log('微任务: MutationObserver');
});
// 监听target节点的变化
observer.observe(target, {
childList: true,
attributes: true,
});
target.setAttribute('data-set', '123');
target.appendChild(document.createElement('span'));
target.setAttribute('style', 'background-color: green;');
执行结果:
makefile
微任务: MutationObserver
详细执行步骤解析:
第1步:DOM操作执行
- 创建div元素并添加到body
- 创建MutationObserver实例并开始监听
- 执行
target.setAttribute('data-set', '123')
- 执行
target.appendChild(document.createElement('span'))
- 执行
target.setAttribute('style', 'background-color: green;')
第2步:微任务队列处理
- 同步DOM操作完成后,MutationObserver检测到变化
- 将观察者回调放入微任务队列
- 执行微任务,输出:
微任务: MutationObserver
关键理解:
- MutationObserver能在DOM改变后、页面渲染前捕获变化
- 多次DOM操作会被批量处理,只触发一次回调
- 这对于性能优化非常重要
queueMicrotask的实际应用
javascript
console.log("同步");
// 批量更新DOM树、CSSOM、layout树和图层合并
queueMicrotask(() => {
// DOM更新了,但不是DOM渲染完了
// 获取元素高度 offsetHeight、scrollTop、getBoundingClientRect()
// 会立即触发重绘重排,影响性能
console.log("微任务: queueMicrotask");
});
console.log("同步结束");
执行结果:
makefile
同步
同步结束
微任务: queueMicrotask
详细执行步骤解析:
第1步:同步任务执行
- 执行
console.log("同步")
,输出:同步
- 遇到
queueMicrotask()
,将回调放入微任务队列 - 执行
console.log("同步结束")
,输出:同步结束
第2步:微任务执行
- 同步任务完成,检查微任务队列
- 执行queueMicrotask回调,输出:
微任务: queueMicrotask
应用场景:
- 在DOM更新后、渲染前执行某些操作
- 避免在微任务中频繁获取DOM几何信息(会触发强制重排)
Promise执行机制深度解析
为了让大家更好地理解Promise与微任务的关系,我们来看一个详细的示例:
javascript
console.log('1');
// Promise构造函数是同步执行的
const promise1 = new Promise((resolve) => {
console.log('2'); // 同步执行
resolve('resolved');
});
// Promise.resolve()是同步的,创建已resolved的Promise
const promise2 = Promise.resolve('immediate');
console.log('3');
// 只有.then()才产生微任务
promise1.then((value) => {
console.log('4: ' + value);
});
// 只有.then()才产生微任务
promise2.then((value) => {
console.log('5: ' + value);
});
console.log('6');
执行结果:
makefile
1
2
3
6
4: resolved
5: immediate
关键理解:
new Promise(executor)
中的executor函数立即同步执行Promise.resolve()
同步创建已resolved的Promise对象- 只有调用
.then()
方法时,才会将回调函数放入微任务队列 - 这就是为什么输出顺序是 1→2→3→6→4→5
实战中的Promise处理
多个Promise的执行顺序
javascript
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(res => {
console.log(res);
});
promise2.then(res => {
console.log(res);
});
promise3.then(res => {
console.log(res);
});
setTimeout(() => {
console.log('六百六十六');
const p4 = Promise.resolve('Promise4');
p4.then(res => {
console.log(res);
});
});
setTimeout(() => {
console.log('演都不演了');
});
console.log('同步end');
执行结果:
sql
同步Start
Promise3
同步end
First Promise
Second Promise
Third Promise
六百六十六
Promise4
演都不演了
详细执行步骤解析:
第1步:同步任务执行
- 执行
console.log('同步Start')
,输出:同步Start
- 创建
promise1
(Promise.resolve()创建已resolved的Promise,这是同步的) - 创建
promise2
(Promise.resolve()创建已resolved的Promise,这是同步的) - 创建
promise3
,执行构造函数中的同步代码 ,输出:Promise3
- 遇到
promise1.then()
,调用.then()才产生微任务,将回调放入微任务队列 - 遇到
promise2.then()
,调用.then()才产生微任务,将回调放入微任务队列 - 遇到
promise3.then()
,调用.then()才产生微任务,将回调放入微任务队列 - 遇到第一个
setTimeout()
,将回调放入宏任务队列 - 遇到第二个
setTimeout()
,将回调放入宏任务队列 - 执行
console.log('同步end')
,输出:同步end
第2步:微任务队列执行
- 同步任务完成,按顺序执行微任务队列中的回调
- 执行promise1.then()回调,输出:
First Promise
- 执行promise2.then()回调,输出:
Second Promise
- 执行promise3.then()回调,输出:
Third Promise
- 微任务队列清空
第3步:第一个宏任务执行
- 从宏任务队列取出第一个setTimeout回调执行
- 输出:
六百六十六
- 在回调中创建
p4
并调用then(),将回调放入微任务队列 - 检查微任务队列,执行p4.then()回调,输出:
Promise4
第4步:第二个宏任务执行
- 微任务队列为空,取出第二个setTimeout回调执行
- 输出:
演都不演了
关键理解:
- Promise构造函数中的代码是同步执行的,不是微任务
- Promise.resolve()、Promise.reject()创建Promise对象是同步的
- 只有.then()、.catch()、.finally()的回调才是微任务
- 已resolved的Promise的then回调会立即进入微任务队列
- 微任务按照进入队列的顺序执行
- 宏任务中产生的微任务会在下一个宏任务前执行
事件循环的完整流程
基于以上所有案例分析,我们可以总结出事件循环的完整执行流程:

执行顺序优先级(从高到低):
- 同步任务 - 直接在调用栈中执行
- 微任务 - 包括Promise.then()、MutationObserver、queueMicrotask等
- 宏任务 - 包括setTimeout、setInterval、DOM事件等
详细执行步骤:
步骤1:执行同步任务
- 执行调用栈中的所有同步代码
- 遇到异步任务时,将回调函数分别放入对应的任务队列
步骤2:检查并执行微任务队列
- 同步任务执行完毕后,立即检查微任务队列
- 依次执行微任务队列中的所有任务
- 如果微任务中又产生新的微任务,继续执行直到队列为空
步骤3:执行一个宏任务
- 微任务队列清空后,从宏任务队列中取出一个任务执行
- 执行完这个宏任务后,不会立即执行下一个宏任务
步骤4:重复步骤2-3
- 再次检查微任务队列,执行所有微任务
- 然后执行下一个宏任务
- 循环往复,直到所有任务完成
关键记忆点:
- 一次事件循环 = 执行一个宏任务 + 清空微任务队列
- 微任务具有"插队"特权,总是在下一个宏任务前执行
- 微任务队列必须完全清空,才会执行下一个宏任务
性能优化建议
- 合理使用微任务:避免在微任务中执行耗时操作
- DOM操作优化:利用MutationObserver批量处理DOM变化
- 避免强制同步布局:在微任务中获取DOM几何信息会触发重排
- Promise链优化:避免过深的Promise嵌套
总结
事件循环机制是JavaScript异步编程的基础,理解它有助于:
- 写出更高效的异步代码
- 避免常见的执行顺序陷阱
- 优化页面性能和用户体验
- 更好地调试异步相关问题
核心要点回顾:
-
Promise本身不是微任务
new Promise(executor)
中的executor是同步执行的Promise.resolve()
创建Promise对象是同步的- 只有
.then()
、.catch()
、.finally()
才产生微任务
-
执行顺序优先级
- 同步任务 > 微任务 > 宏任务
- 微任务队列必须完全清空后才执行宏任务
-
事件循环流程
- 一次事件循环 = 执行一个宏任务 + 清空微任务队列
- 微任务具有"插队"特权
记住这个口诀:同步优先,微任务次之,宏任务最后。每次宏任务执行完毕,都要清空微任务队列,这就是事件循环的核心逻辑。
在实际开发中,多观察、多实践,你会发现事件循环不仅是理论知识,更是提升代码质量的有力工具。