登录
首页 >  文章 >  前端

async函数执行顺序全解析

时间:2025-07-14 23:45:34 262浏览 收藏

偷偷努力,悄无声息地变强,然后惊艳所有人!哈哈,小伙伴们又来学习啦~今天我将给大家介绍《async函数执行顺序详解》,这篇文章主要会讲到等等知识点,不知道大家对其都有多少了解,下面我们就一起来看一吧!当然,非常希望大家能多多评论,给出合理的建议,我们一起学习,一起进步!

async函数的执行顺序基于Promise和事件循环,是一种非阻塞的“暂停与恢复”机制。1.当调用async函数时,它会立即执行同步代码,直到遇到第一个await表达式;2.此时函数会挂起,并将后续代码作为微任务放入队列,控制权交还主线程;3.被await的Promise完成后,函数从暂停处恢复执行;4.整个过程不阻塞主线程,确保应用响应性;5.await不会真正并行执行任务,而是利用事件循环实现异步协作;6.错误处理通过try...catch捕获await的拒绝,未被捕获的拒绝需在外部用.catch()处理;7.优化性能的关键在于识别任务依赖关系,独立任务使用Promise.all等实现并行,减少总耗时;8.避免过早await,可在后台先启动Promise并在需要结果时再等待,以提升效率。

async函数的执行顺序解析

async 函数的执行顺序,说到底,并不是真正意义上的并行,它更像是一种智能的“暂停与恢复”机制。当你调用一个 async 函数时,它会立即开始执行,直到遇到第一个 await 表达式。这时,函数会“暂停”自己的执行,将控制权交还给 JavaScript 事件循环。被 await 的操作完成后,函数会从暂停的地方继续执行。这个过程是非阻塞的,意味着在 async 函数等待期间,主线程可以自由地执行其他任务,确保了用户界面的响应性或服务器的吞吐量。

async函数的执行顺序解析

解决方案

要深入理解 async 函数的执行顺序,我们得从它的本质——基于 Promise 和事件循环——说起。async/await 是 ES2017 引入的语法糖,它让异步代码看起来更像同步代码,极大地提升了可读性。

当一个 async 函数被调用时,它会立即执行函数体内的同步代码,直到遇到第一个 await 关键字。一旦 await 了一个 Promise(或者一个非 Promise 值,它会被立即包装成一个已解决的 Promise),async 函数的执行就会被“挂起”。这里的“挂起”并非阻塞,而是将 await 之后的所有代码作为回调,放入微任务队列(Microtask Queue)中。

async函数的执行顺序解析

与此同时,控制权会立即返回到调用 async 函数的地方,或者说,返回到主线程。主线程会继续执行它当前的任务,比如后续的同步代码、或下一个宏任务(如 setTimeout 的回调)。只有当主线程的调用栈清空后,事件循环才会检查微任务队列。如果队列中有任务,它们会被优先执行,这其中就包括了被 await 挂起的 async 函数的后续部分。

举个例子,我们来看看这段代码:

async函数的执行顺序解析
async function exampleAsync() {
  console.log('A: 进入 async 函数');
  await new Promise(resolve => setTimeout(() => {
    console.log('B: Promise 内部完成');
    resolve();
  }, 0));
  console.log('C: await 之后');
}

console.log('D: 全局同步代码 1');
exampleAsync();
console.log('E: 全局同步代码 2');

// 实际输出顺序:
// D: 全局同步代码 1
// A: 进入 async 函数
// E: 全局同步代码 2
// B: Promise 内部完成
// C: await 之后

解析一下:

  1. D: 全局同步代码 1 立即输出。
  2. exampleAsync() 被调用,A: 进入 async 函数 立即输出。
  3. 遇到 await new Promise(...)。此时,async 函数的执行被“暂停”,console.log('C: await 之后') 这部分代码被推入微任务队列。同时,setTimeout 将其回调(打印 Bresolve Promise)推入宏任务队列。
  4. 控制权返回主线程,E: 全局同步代码 2 立即输出。
  5. 主线程同步代码执行完毕,事件循环开始工作。它首先检查宏任务队列,发现 setTimeout 的回调。
  6. B: Promise 内部完成 输出,并且 Promise resolve
  7. Promise 解决后,await 表达式被认为是完成了,它将 async 函数中剩余的部分(打印 C 的部分)从微任务队列取出,放入调用栈执行。
  8. C: await 之后 输出。

这个流程清晰地展示了 async/await 如何与事件循环协同工作,实现非阻塞的异步操作。

为什么 await 不会阻塞主线程?它的内部机制是怎样的?

这其实是 async/await 设计中最精妙的部分,也是它与传统阻塞式编程模型(比如一些多线程语言中的 sleep())最根本的区别。await 之所以不阻塞主线程,核心在于它利用了 JavaScript 的“事件循环”(Event Loop)机制和“微任务队列”(Microtask Queue)。

await 一个 Promise 时,async 函数并不会傻傻地停在那里,等待 Promise 完成。相反,它会做几件事:

  1. 保存上下文: async 函数会记住它当前执行到的位置,以及所有相关的局部变量状态。
  2. 让出控制权: 它会立即将控制权交还给事件循环,允许主线程继续执行当前调用栈中剩下的任务,或者处理下一个宏任务(如用户交互、网络请求的回调、setTimeout 等)。
  3. 注册回调: await 表达式背后的 Promise 一旦解决(无论是成功还是失败),它会把 async 函数中 await 之后的代码片段作为一个“微任务”推入微任务队列。

微任务队列的优先级非常高。在每一次宏任务(比如一个完整的脚本执行、一个 setTimeout 回调、一个用户事件处理)完成之后,事件循环都会优先清空所有堆积的微任务,然后再去处理下一个宏任务。

所以,当 await 暂停时,主线程并没有闲着,它在忙着处理其他任务。只有当主线程的任务都处理完了,并且 await 的 Promise 也已经解决了,事件循环才会把之前推入微任务队列的 async 函数的剩余部分拿出来执行。这种“合作式多任务处理”的方式,避免了长时间的阻塞,确保了 JavaScript 应用的响应性和流畅性。它不是真正的并行,而是一种高效的任务调度。

async 函数中的错误处理和常规同步代码有什么不同?

async 函数中的错误处理,乍一看和同步代码的 try...catch 块很相似,但由于其异步的本质,还是有一些微妙的区别和需要注意的地方。

最直接的区别在于,try...catchasync 函数中可以捕获到由 await 表达式抛出的错误(即被 await 的 Promise 拒绝)。这让异步错误处理变得异常简洁和直观。

async function fetchDataWithError() {
  try {
    console.log('尝试获取数据...');
    // 模拟一个网络请求失败或服务器返回错误
    const response = await new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(new Error('网络请求失败:服务器无响应!'));
      }, 500);
    });
    console.log('数据获取成功:', response); // 这行代码不会被执行
  } catch (error) {
    console.error('捕获到错误:', error.message);
  } finally {
    console.log('无论成功失败,都会执行');
  }
}

async function anotherAsyncFunction() {
  console.log('另一个异步函数开始');
  // 如果这里抛出的错误没有被内部捕获,它会拒绝 anotherAsyncFunction 返回的 Promise
  await Promise.reject('这是一个未被内部捕获的错误!');
  console.log('这行代码也不会执行');
}

fetchDataWithError();

// 对于没有内部 try...catch 的 async 函数,需要在外部捕获其返回的 Promise 的拒绝
anotherAsyncFunction().catch(err => {
  console.error('在外部捕获到另一个异步函数的错误:', err);
});

console.log('同步代码继续执行...');

关键点:

  1. try...catch 捕获 await 的拒绝: 这是 async/await 最大的便利。当 await 一个被拒绝的 Promise 时,它会像同步代码抛出错误一样,立即跳转到最近的 catch 块。
  2. 未捕获的拒绝: 如果 async 函数内部抛出了错误,但没有被 try...catch 捕获,那么这个 async 函数返回的 Promise 就会变成拒绝状态。这意味着你需要在这个 async 函数的调用链外部,使用 .catch() 方法来处理这个拒绝。这和处理普通 Promise 的拒绝是一样的。
  3. 同步错误: try...catch 也能捕获 async 函数内部的同步错误,比如一个引用错误或类型错误,这和在普通同步函数中一样。
  4. finally 块: finally 块在 try...catch 结束后,无论是否有错误发生,都会被执行,这在清理资源时非常有用。

我个人觉得,理解 async 函数的错误处理,最重要的是记住 async 函数本质上还是返回一个 Promise。所以,当内部的 try...catch 无法处理所有错误时,最终还是会回归到 Promise 的错误处理机制上,即通过 .catch() 来处理未被捕获的拒绝。这其实提供了一个非常灵活的错误处理策略:你可以在函数内部进行细粒度的错误处理,也可以将错误冒泡到调用栈的更高层级进行统一处理。

如何优化 async 函数以提高性能?并行执行与串行执行的选择。

优化 async 函数的性能,很多时候归结为一句话:在不影响逻辑依赖的前提下,尽可能地并行执行独立的异步操作。 async/await 默认是串行执行的,这在处理有前后依赖关系的任务时非常直观和安全,但如果任务之间没有依赖,串行执行就会白白浪费时间。

  1. 串行执行 (默认行为)

    当你连续 await 多个操作时,它们是串行执行的。这意味着第二个操作必须等待第一个操作完成后才能开始。

    async function fetchSequentially() {
      console.time('Sequential Fetch');
      const user = await fetch('/api/user').then(res => res.json());
      const posts = await fetch(`/api/posts?userId=${user.id}`).then(res => res.json()); // 依赖 user.id
      console.log('用户和帖子数据已获取 (串行)');
      console.timeEnd('Sequential Fetch');
      return { user, posts };
    }
    // 这种情况下,串行是必要的,因为获取帖子需要用户ID。

    这种模式适合有依赖关系的任务流,代码逻辑清晰,易于理解。

  2. 并行执行 (Promise.all)

    如果你的多个异步操作之间没有依赖关系,它们完全可以同时开始执行,然后等待所有操作都完成。Promise.all 就是为此而生的。

    async function fetchConcurrently() {
      console.time('Concurrent Fetch');
      // 这两个请求可以同时发出,因为它们互不依赖
      const userPromise = fetch('/api/user').then(res => res.json());
      const productsPromise = fetch('/api/products').then(res => res.json());
    
      const [user, products] = await Promise.all([userPromise, productsPromise]);
    
      console.log('用户和产品数据已获取 (并行)');
      console.timeEnd('Concurrent Fetch');
      return { user, products };
    }
    // fetchConcurrently();

    使用 Promise.all 会显著减少总的等待时间,因为它等待的是最慢的那个 Promise 完成的时间,而不是所有 Promise 时间的总和。这是 async 函数性能优化的一个核心策略。

  3. 并行执行 (Promise.race, Promise.any, Promise.allSettled)

    除了 Promise.all,还有其他一些用于不同并行场景的方法:

    • Promise.race([p1, p2]): 只要有一个 Promise 解决或拒绝,就立即返回结果。适合竞速场景,比如哪个服务器响应快就用哪个。
    • Promise.any([p1, p2]): 只要有一个 Promise 解决,就立即返回该 Promise 的值。如果所有 Promise 都拒绝,则抛出 AggregateError。适合需要任一成功结果的场景。
    • Promise.allSettled([p1, p2]): 等待所有 Promise 都“落定”(无论是解决还是拒绝),并返回一个包含每个 Promise 状态和结果的数组。这在你想知道所有操作的结果,即使某些失败了也想继续处理时非常有用。
  4. 避免不必要的 await

    有时候,你可能在 async 函数的开头就 await 了一个 Promise,但实际上这个 Promise 的结果在函数体很后面才需要。这种情况下,你可以先启动 Promise,让它在后台运行,等到真正需要结果的时候再 await

    async function smartFetch() {
      const initialDataPromise = fetch('/api/initial-data').then(res => res.json()); // 立即启动
    
      // 在等待 initialDataPromise 解决的同时,可以执行一些不依赖它的同步计算
      console.log('执行一些不依赖 initialData 的同步操作...');
      const processedLocalData = someComplexSyncCalculation();
    
      // 当需要 initialData 的时候再 await
      const initialData = await initialDataPromise;
    
      console.log('Initial data:', initialData);
      console.log('Processed local data:', processedLocalData);
    }
    // smartFetch();

    这种模式允许你在等待异步操作的同时,充分利用 CPU 周期执行同步任务,从而提升整体效率。

优化 async 函数,并不是简单地将所有 await 都替换为 Promise.all。它要求你对业务逻辑有清晰的理解,知道哪些任务是独立的,哪些任务有依赖。合理地选择串行或并行执行,是编写高性能、响应式 async 代码的关键。我个人在实践中发现,很多性能瓶颈往往就出现在那些可以并行却被不经意串行执行的异步操作上。

到这里,我们也就讲完了《async函数执行顺序全解析》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于的知识点!

相关阅读
更多>
最新阅读
更多>
课程推荐
更多>