Promise 和 async/await 是 JavaScript 中进行异步编程的两种重要方式。要理解它们在 event loop 中的调用机制,需要深入了解 JavaScript 的执行模型,包括 call stack(调用栈)、event loop(事件循环)、microtask queue(微任务队列)和 macrotask queue(宏任务队列)。
JavaScript 执行模型
JavaScript 是单线程的,这意味着它一次只能执行一个任务。为了实现异步操作,JavaScript 使用了一个基于事件驱动的模型,event loop。这个模型的主要组成部分包括:
- Call Stack:调用栈,用于跟踪正在执行的函数。
- Web APIs:浏览器提供的 API,例如 setTimeout、DOM 事件等。
- Task Queue:任务队列,用于存储异步任务的回调。
- Event Loop:事件循环,负责协调调用栈和任务队列的执行。
Promise 和 Event Loop
Promise 是一个表示异步操作最终完成或失败的对象。它有三种状态:pending(待定)、fulfilled(已完成)和 rejected(已拒绝)。Promise 的主要特点是它能够链接多个异步操作,使得代码更具可读性。
当 Promise 产生时,它会立即执行内部代码,但 then 和 catch 中的回调会被放入 microtask queue。
示例:
console.log('script start');
const promise1 = new Promise((resolve, reject) => {
console.log('promise1 start');
resolve('promise1 result');
}).then(result => {
console.log(result);
});
console.log('script end');
在这个例子中,执行顺序如下:
console.log('script start')
被放入调用栈并执行。new Promise
被放入调用栈,console.log('promise1 start')
被执行。resolve('promise1 result')
被调用,但 then 回调被放入 microtask queue。console.log('script end')
被放入调用栈并执行。- 调用栈清空后,event loop 检查 microtask queue 并执行 then 回调。
最终输出顺序是:
script start
promise1 start
script end
promise1 result
async/await 和 Event Loop
async/await 是 ES2017 引入的一种更简洁的异步编程方式。async 函数返回一个 Promise,await 可以暂停 async 函数的执行,等待 Promise 被解决。
示例:
console.log('script start');
async function asyncFunction() {
console.log('asyncFunction start');
const result = await new Promise((resolve, reject) => {
console.log('promise start');
resolve('promise result');
});
console.log(result);
}
asyncFunction();
console.log('script end');
在这个例子中,执行顺序如下:
console.log('script start')
被放入调用栈并执行。asyncFunction
被放入调用栈并执行,console.log('asyncFunction start')
被执行。new Promise
被放入调用栈,console.log('promise start')
被执行。resolve('promise result')
被调用,但 await 暂停了 asyncFunction,回调被放入 microtask queue。console.log('script end')
被放入调用栈并执行。- 调用栈清空后,event loop 检查 microtask queue 并执行 await 之后的代码。
最终输出顺序是:
script start
asyncFunction start
promise start
script end
promise result
更复杂的示例
为了更深入地理解 Promise 和 async/await 在 event loop 中的调用机制,可以看一个更复杂的示例。
示例:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
async function asyncFunction() {
console.log('asyncFunction start');
await Promise.resolve();
console.log('asyncFunction end');
}
asyncFunction();
console.log('script end');
执行顺序如下:
console.log('script start')
被放入调用栈并执行。setTimeout
回调被放入 macrotask queue。Promise.resolve().then
被放入 microtask queue。asyncFunction
被放入调用栈并执行,console.log('asyncFunction start')
被执行。await Promise.resolve()
被放入 microtask queue,asyncFunction 暂停。console.log('script end')
被放入调用栈并执行。调用栈清空后,event loop 检查 microtask queue 并执行:
Promise.resolve().then
中的回调,console.log('promise1')
。console.log('promise2')
。await Promise.resolve()
之后的代码,console.log('asyncFunction end')
。
- 最后,event loop 检查 macrotask queue 并执行
setTimeout
回调。
最终输出顺序是:
script start
asyncFunction start
script end
promise1
promise2
asyncFunction end
setTimeout
内部工作原理
Promise 的内部机制
当创建一个 Promise 时,构造函数内的代码会立即执行。但 then 和 catch 回调会被放入 microtask queue,等待当前事件循环结束后执行。
const promise = new Promise((resolve, reject) => {
console.log('Promise executor');
resolve();
});
promise.then(() => {
console.log('Promise then');
});
console.log('End');
执行顺序:
console.log('Promise executor')
。resolve
被调用,将 then 回调放入 microtask queue。console.log('End')
。- microtask queue 被处理,执行 then 回调,
console.log('Promise then')
。
最终输出:
Promise executor
End
Promise then
async/await 的内部机制
async 函数会返回一个 Promise,await 表达式会暂停 async 函数的执行,等待 Promise 解决后继续执行。await 表达式相当于 Promise 的 then 方法,因此它的回调也会放入 microtask queue。
async function asyncFunction() {
console.log('asyncFunction start');
await Promise.resolve();
console.log('asyncFunction end');
}
asyncFunction();
console.log('End');
执行顺序:
console.log('asyncFunction start')
。await Promise.resolve()
将回调放入 microtask queue,暂停 asyncFunction。console.log('End')
。- microtask queue 被处理,执行 await 之后的代码,
console.log('asyncFunction end')
。
最终输出:
asyncFunction start
End
asyncFunction end
微任务和宏任务
微任务(microtask)和宏任务(macrotask)的区别在于它们在 event loop 中的执行时机。微任务包括 Promise 的 then/catch/finally 回调和 MutationObserver 回调。宏任务包括 setTimeout、setInterval、setImmediate、I/O 事件和 UI 渲染。
每次事件循环,首先处理所有微任务队列中的任务,然后处理一个宏任务。
示例:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
执行顺序:
console.log('script start')
。setTimeout
回调放入宏任务队列。Promise.resolve().then
回调放入微任务队列。console.log('script end')
。- 处理微任务队列,执行
promise1
和promise2
。 - 处理宏任务队列,执行
setTimeout
回调。
最终输出:
script start
script end
promise1
promise2
setTimeout
实践应用
了解 Promise 和 async/await 在 event loop 中的调用机制对编写高效、可维护的异步代码非常重要。合理使用 microtask 和 macrotask,可以优化代码执行顺序,避免不必要的延迟。
示例:顺序执行多个异步操作
function fetchData(url) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Data from ${url}`);
}, 1000);
});
}
async function fetchSequentially() {
console.log('Start fetching');
const data1 = await fetchData('url1');
console.log(data1);
const data2 = await fetchData('url2');
console.log(data2);
console.log('Finished fetching');
}
fetchSequentially();
在这个例子中,fetchSequentially
函数顺序执行两个异步操作,确保在第一个操作完成后才开始第二个操作。
最终输出:
Start
fetching
Data from url1
Data from url2
Finished fetching
结论
理解 Promise 和 async/await 在 event loop 中的调用机制,可以帮助开发者编写更高效和可维护的异步代码。通过掌握微任务和宏任务的执行时机,开发者可以优化代码执行顺序,避免不必要的延迟,提高应用性能。这个知识不仅适用于 JavaScript,也适用于其他基于事件驱动模型的编程语言和环境。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。