microtask queue 与事件循环:Promise.then() 为何比 setTimeout 先执行?
理解 JavaScript 事件循环
JavaScript 是单线程的,但它通过事件循环机制实现了异步操作。理解事件循环对于掌握 JavaScript 的执行顺序至关重要,特别是当涉及到 Promise 和定时器等异步操作时。
事件循环的基本概念
javascript
console.log('1. 同步代码');
setTimeout(() => {
console.log('3. setTimeout 回调');
}, 0);
Promise.resolve().then(() => {
console.log('2. Promise 回调');
});
console.log('1. 同步代码结束');
// 输出顺序:
// 1. 同步代码
// 1. 同步代码结束
// 2. Promise 回调
// 3. setTimeout 回调任务队列的分类
JavaScript 的事件循环维护多个任务队列,主要包括:
- 宏任务队列(Macrotask Queue):包括 setTimeout、setInterval、I/O 操作等
- 微任务队列(Microtask Queue):包括 Promise.then、queueMicrotask、MutationObserver 等
宏任务与微任务的执行顺序
javascript
console.log('Start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
console.log('End');
// 输出顺序:
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// setTimeout 2微任务队列的详细分析
Promise.then 属于微任务
javascript
console.log('1. 开始');
setTimeout(() => {
console.log('4. setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('2. Promise 第一个 then');
return '来自第一个 then';
})
.then((value) => {
console.log('3. Promise 第二个 then:', value);
});
console.log('1. 结束');
// 输出顺序:
// 1. 开始
// 1. 结束
// 2. Promise 第一个 then
// 3. Promise 第二个 then: 来自第一个 then
// 4. setTimeout其他微任务类型
javascript
// queueMicrotask 示例
console.log('1. 开始');
queueMicrotask(() => {
console.log('3. queueMicrotask 回调');
});
Promise.resolve().then(() => {
console.log('2. Promise 回调');
});
console.log('1. 结束');
// 输出顺序:
// 1. 开始
// 1. 结束
// 2. Promise 回调
// 3. queueMicrotask 回调事件循环的执行流程
事件循环的执行遵循以下步骤:
- 执行所有同步代码
- 执行所有微任务(清空微任务队列)
- 执行一个宏任务(从宏任务队列取出一个任务执行)
- 重复步骤 2 和 3
详细的执行流程示例
javascript
console.log('Script 开始');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('Promise 在 setTimeout 1 中');
});
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('setTimeout 在 Promise 1 中');
}, 0);
}).then(() => {
console.log('Promise 2');
});
console.log('Script 结束');
// 输出顺序:
// Script 开始
// Script 结束
// Promise 1
// Promise 2
// setTimeout 1
// Promise 在 setTimeout 1 中
// setTimeout 2
// setTimeout 在 Promise 1 中微任务优先级的深入理解
微任务在每次事件循环中的执行
javascript
// 演示微任务在每次宏任务后都会执行
setTimeout(() => {
console.log('宏任务 1');
Promise.resolve().then(() => {
console.log('微任务 1');
});
}, 0);
setTimeout(() => {
console.log('宏任务 2');
Promise.resolve().then(() => {
console.log('微任务 2');
});
}, 0);
// 输出顺序:
// 宏任务 1
// 微任务 1
// 宏任务 2
// 微任务 2微任务队列的实时更新
javascript
console.log('开始');
Promise.resolve().then(() => {
console.log('第一个微任务');
return Promise.resolve('来自第一个微任务');
}).then((value) => {
console.log('第二个微任务:', value);
// 在微任务中添加新的微任务
Promise.resolve().then(() => {
console.log('第三个微任务');
});
});
Promise.resolve().then(() => {
console.log('第四个微任务');
});
console.log('结束');
// 输出顺序:
// 开始
// 结束
// 第一个微任务
// 第四个微任务
// 第二个微任务: 来自第一个微任务
// 第三个微任务实际应用场景
DOM 操作的响应
javascript
// 在 DOM 操作后立即响应
function updateDOM() {
const element = document.getElementById('myElement');
element.textContent = '新内容';
// 使用微任务确保 DOM 更新后立即执行某些操作
Promise.resolve().then(() => {
console.log('DOM 已更新');
// 执行依赖于 DOM 更新的操作
});
}异步错误处理
javascript
function riskyOperation() {
// 模拟可能出错的异步操作
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('成功');
} else {
reject(new Error('失败'));
}
}, 100);
});
}
// 使用微任务处理错误
riskyOperation()
.then(result => {
console.log('操作成功:', result);
})
.catch(error => {
console.error('操作失败:', error.message);
// 在微任务中进行清理工作
Promise.resolve().then(() => {
console.log('执行清理工作');
});
});常见误区和陷阱
误解执行顺序
javascript
// 常见的误解
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
// 很多人错误地认为 setTimeout 会先执行,但实际上 Promise 会先执行嵌套异步操作的复杂性
javascript
// 复杂的嵌套情况
setTimeout(() => {
console.log('Outer setTimeout');
Promise.resolve().then(() => {
console.log('Inner Promise 1');
}).then(() => {
console.log('Inner Promise 2');
});
setTimeout(() => {
console.log('Inner setTimeout');
}, 0);
}, 0);
Promise.resolve().then(() => {
console.log('Outer Promise');
});
// 输出顺序需要仔细分析事件循环浏览器兼容性和差异
不同浏览器的实现
javascript
// 某些老旧浏览器可能有不同的实现
// 现代浏览器都遵循相同的事件循环规范
// 可以使用以下方式检测微任务支持
if (typeof Promise !== 'undefined') {
console.log('支持 Promise 微任务');
}
if (typeof queueMicrotask !== 'undefined') {
console.log('支持 queueMicrotask');
}最佳实践
1. 理解执行顺序
javascript
// 始终考虑事件循环的影响
function asyncOperation() {
console.log('1. 同步执行');
setTimeout(() => {
console.log('3. 宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('2. 微任务');
});
console.log('1. 同步执行结束');
}2. 合理使用微任务
javascript
// 在需要立即响应时使用微任务
function immediateCallback() {
// 执行一些同步操作
performSyncOperation();
// 使用微任务确保在当前事件循环末尾执行回调
Promise.resolve().then(() => {
notifyListeners();
});
}3. 避免阻塞微任务队列
javascript
// 避免在微任务中执行耗时操作
Promise.resolve().then(() => {
// 不要在这里执行长时间运行的操作
// heavyComputation(); // 这会阻塞其他微任务
// 可以将耗时操作移到宏任务中
setTimeout(() => {
heavyComputation();
}, 0);
});总结
JavaScript 的事件循环机制通过宏任务和微任务队列的协调工作,实现了异步操作的有序执行。微任务(如 Promise.then)总是优先于宏任务(如 setTimeout)执行,这是因为微任务队列在每次事件循环迭代中都会被清空。理解这一机制对于编写正确的异步代码、预测执行顺序以及调试异步问题至关重要。Promise.then 比 setTimeout 先执行正是因为 Promise 属于微任务,而 setTimeout 属于宏任务,微任务具有更高的优先级。