前言
在 JavaScript 开发中,我们经常会遇到 Promise 和 setTimeout 一起使用的情况。有时候,我们会发现 Promise 的回调比 setTimeout 的回调先执行,这可能会让人感到困惑。今天,我们就来深入探讨一下这个现象。
一、事件循环基础
JavaScript 是单线程语言,但它通过事件循环实现了并发操作。事件循环的工作原理可以简单理解为:浏览器或 Node.js 运行时维护了一个任务队列,当主线程空闲时,会从队列中取出任务执行。
任务分为两种类型:
- • 宏任务(Macrotask) :包括 setTimeout、setInterval、I/O 操作等。
- • 微任务(Microtask) :包括 Promise.then/catch/finally 回调、MutationObserver 等。
事件循环的执行顺序是:先执行当前所有微任务,再执行下一个宏任务。
二、Promise 和 setTimeout 的执行顺序
1. Promise 是微任务
当一个 Promise 被创建并调用 then 方法时,它的回调会被加入到微任务队列中。微任务队列的特点是:一旦当前同步代码执行完毕,微任务队列中的任务会立即执行[^34^]。
2. setTimeout 是宏任务
setTimeout 的回调会被加入到宏任务队列中。宏任务队列的特点是:只有在当前所有微任务执行完毕后,才会执行下一个宏任务。
3. 执行顺序示例
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
输出结果:
Start
End
Promise
Timeout
在这个例子中,Promise 的回调比 setTimeout 的回调先执行,因为 Promise 是微任务,而 setTimeout 是宏任务。
三、深入理解事件循环
1. 事件循环的阶段
事件循环分为以下几个阶段:
- 1. 当前任务执行:执行当前的同步代码。
- 2. 微任务处理:处理微任务队列中的所有任务。
- 3. 渲染更新:更新页面(浏览器环境)。
- 4. 宏任务处理:处理宏任务队列中的下一个任务。
2. Promise 和 setTimeout 在事件循环中的位置
- • Promise 的回调:在微任务处理阶段执行。
- • setTimeout 的回调:在宏任务处理阶段执行。
3. 为什么 Promise 更快?
因为微任务队列的优先级高于宏任务队列。一旦当前同步代码执行完毕,事件循环会立即处理微任务队列中的任务,然后再处理宏任务队列中的任务。
四、代码示例与分析
示例 1:Promise 和 setTimeout 的顺序
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
});
setTimeout(() => {
console.log('Timeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 2');
});
console.log('End');
输出结果:
Start
End
Promise 1
Promise 2
Timeout 1
Timeout 2
示例 2:Promise 和 setTimeout 的嵌套
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => {
console.log('Promise in Timeout 1');
});
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
setTimeout(() => {
console.log('Timeout in Promise 1');
}, 0);
});
console.log('End');
输出结果:
Start
End
Promise 1
Timeout 1
Promise in Timeout 1
Timeout in Promise 1
在这个例子中,Promise 的回调仍然比 setTimeout 的回调先执行,即使它们是嵌套在彼此内部的。
五、总结
Promise 的回调比 setTimeout 的回调先执行,是因为 Promise 是微任务,而 setTimeout 是宏任务。微任务队列的优先级高于宏任务队列,因此 Promise 的回调会在 setTimeout 的回调之前执行。
理解事件循环的工作原理对于编写高效的 JavaScript 代码至关重要。通过合理利用微任务和宏任务的特点,我们可以更好地控制代码的执行顺序。