头图

Promise 和 async/await 是 JavaScript 中进行异步编程的两种重要方式。要理解它们在 event loop 中的调用机制,需要深入了解 JavaScript 的执行模型,包括 call stack(调用栈)、event loop(事件循环)、microtask queue(微任务队列)和 macrotask queue(宏任务队列)。

JavaScript 执行模型

JavaScript 是单线程的,这意味着它一次只能执行一个任务。为了实现异步操作,JavaScript 使用了一个基于事件驱动的模型,event loop。这个模型的主要组成部分包括:

  1. Call Stack:调用栈,用于跟踪正在执行的函数。
  2. Web APIs:浏览器提供的 API,例如 setTimeout、DOM 事件等。
  3. Task Queue:任务队列,用于存储异步任务的回调。
  4. 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');

在这个例子中,执行顺序如下:

  1. console.log('script start') 被放入调用栈并执行。
  2. new Promise 被放入调用栈,console.log('promise1 start') 被执行。
  3. resolve('promise1 result') 被调用,但 then 回调被放入 microtask queue。
  4. console.log('script end') 被放入调用栈并执行。
  5. 调用栈清空后,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');

在这个例子中,执行顺序如下:

  1. console.log('script start') 被放入调用栈并执行。
  2. asyncFunction 被放入调用栈并执行,console.log('asyncFunction start') 被执行。
  3. new Promise 被放入调用栈,console.log('promise start') 被执行。
  4. resolve('promise result') 被调用,但 await 暂停了 asyncFunction,回调被放入 microtask queue。
  5. console.log('script end') 被放入调用栈并执行。
  6. 调用栈清空后,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');

执行顺序如下:

  1. console.log('script start') 被放入调用栈并执行。
  2. setTimeout 回调被放入 macrotask queue。
  3. Promise.resolve().then 被放入 microtask queue。
  4. asyncFunction 被放入调用栈并执行,console.log('asyncFunction start') 被执行。
  5. await Promise.resolve() 被放入 microtask queue,asyncFunction 暂停。
  6. console.log('script end') 被放入调用栈并执行。
  7. 调用栈清空后,event loop 检查 microtask queue 并执行:

    • Promise.resolve().then 中的回调,console.log('promise1')
    • console.log('promise2')
    • await Promise.resolve() 之后的代码,console.log('asyncFunction end')
  8. 最后,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');

执行顺序:

  1. console.log('Promise executor')
  2. resolve 被调用,将 then 回调放入 microtask queue。
  3. console.log('End')
  4. 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');

执行顺序:

  1. console.log('asyncFunction start')
  2. await Promise.resolve() 将回调放入 microtask queue,暂停 asyncFunction。
  3. console.log('End')
  4. 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');

执行顺序:

  1. console.log('script start')
  2. setTimeout 回调放入宏任务队列。
  3. Promise.resolve().then 回调放入微任务队列。
  4. console.log('script end')
  5. 处理微任务队列,执行 promise1promise2
  6. 处理宏任务队列,执行 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,也适用于其他基于事件驱动模型的编程语言和环境。


注销
1k 声望1.6k 粉丝

invalid