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
函数时,它会立即开始执行,直到遇到第一个 await
表达式。这时,函数会“暂停”自己的执行,将控制权交还给 JavaScript 事件循环。被 await
的操作完成后,函数会从暂停的地方继续执行。这个过程是非阻塞的,意味着在 async
函数等待期间,主线程可以自由地执行其他任务,确保了用户界面的响应性或服务器的吞吐量。

解决方案
要深入理解 async
函数的执行顺序,我们得从它的本质——基于 Promise 和事件循环——说起。async/await
是 ES2017 引入的语法糖,它让异步代码看起来更像同步代码,极大地提升了可读性。
当一个 async
函数被调用时,它会立即执行函数体内的同步代码,直到遇到第一个 await
关键字。一旦 await
了一个 Promise(或者一个非 Promise 值,它会被立即包装成一个已解决的 Promise),async
函数的执行就会被“挂起”。这里的“挂起”并非阻塞,而是将 await
之后的所有代码作为回调,放入微任务队列(Microtask Queue)中。

与此同时,控制权会立即返回到调用 async
函数的地方,或者说,返回到主线程。主线程会继续执行它当前的任务,比如后续的同步代码、或下一个宏任务(如 setTimeout
的回调)。只有当主线程的调用栈清空后,事件循环才会检查微任务队列。如果队列中有任务,它们会被优先执行,这其中就包括了被 await
挂起的 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 之后
解析一下:
D: 全局同步代码 1
立即输出。exampleAsync()
被调用,A: 进入 async 函数
立即输出。- 遇到
await new Promise(...)
。此时,async
函数的执行被“暂停”,console.log('C: await 之后')
这部分代码被推入微任务队列。同时,setTimeout
将其回调(打印B
并resolve
Promise)推入宏任务队列。 - 控制权返回主线程,
E: 全局同步代码 2
立即输出。 - 主线程同步代码执行完毕,事件循环开始工作。它首先检查宏任务队列,发现
setTimeout
的回调。 B: Promise 内部完成
输出,并且 Promiseresolve
。- Promise 解决后,
await
表达式被认为是完成了,它将async
函数中剩余的部分(打印C
的部分)从微任务队列取出,放入调用栈执行。 C: await 之后
输出。
这个流程清晰地展示了 async/await
如何与事件循环协同工作,实现非阻塞的异步操作。
为什么 await
不会阻塞主线程?它的内部机制是怎样的?
这其实是 async/await
设计中最精妙的部分,也是它与传统阻塞式编程模型(比如一些多线程语言中的 sleep()
)最根本的区别。await
之所以不阻塞主线程,核心在于它利用了 JavaScript 的“事件循环”(Event Loop)机制和“微任务队列”(Microtask Queue)。
当 await
一个 Promise 时,async
函数并不会傻傻地停在那里,等待 Promise 完成。相反,它会做几件事:
- 保存上下文:
async
函数会记住它当前执行到的位置,以及所有相关的局部变量状态。 - 让出控制权: 它会立即将控制权交还给事件循环,允许主线程继续执行当前调用栈中剩下的任务,或者处理下一个宏任务(如用户交互、网络请求的回调、
setTimeout
等)。 - 注册回调:
await
表达式背后的 Promise 一旦解决(无论是成功还是失败),它会把async
函数中await
之后的代码片段作为一个“微任务”推入微任务队列。
微任务队列的优先级非常高。在每一次宏任务(比如一个完整的脚本执行、一个 setTimeout
回调、一个用户事件处理)完成之后,事件循环都会优先清空所有堆积的微任务,然后再去处理下一个宏任务。
所以,当 await
暂停时,主线程并没有闲着,它在忙着处理其他任务。只有当主线程的任务都处理完了,并且 await
的 Promise 也已经解决了,事件循环才会把之前推入微任务队列的 async
函数的剩余部分拿出来执行。这种“合作式多任务处理”的方式,避免了长时间的阻塞,确保了 JavaScript 应用的响应性和流畅性。它不是真正的并行,而是一种高效的任务调度。
async
函数中的错误处理和常规同步代码有什么不同?
async
函数中的错误处理,乍一看和同步代码的 try...catch
块很相似,但由于其异步的本质,还是有一些微妙的区别和需要注意的地方。
最直接的区别在于,try...catch
在 async
函数中可以捕获到由 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('同步代码继续执行...');
关键点:
try...catch
捕获await
的拒绝: 这是async/await
最大的便利。当await
一个被拒绝的 Promise 时,它会像同步代码抛出错误一样,立即跳转到最近的catch
块。- 未捕获的拒绝: 如果
async
函数内部抛出了错误,但没有被try...catch
捕获,那么这个async
函数返回的 Promise 就会变成拒绝状态。这意味着你需要在这个async
函数的调用链外部,使用.catch()
方法来处理这个拒绝。这和处理普通 Promise 的拒绝是一样的。 - 同步错误:
try...catch
也能捕获async
函数内部的同步错误,比如一个引用错误或类型错误,这和在普通同步函数中一样。 finally
块:finally
块在try...catch
结束后,无论是否有错误发生,都会被执行,这在清理资源时非常有用。
我个人觉得,理解 async
函数的错误处理,最重要的是记住 async
函数本质上还是返回一个 Promise。所以,当内部的 try...catch
无法处理所有错误时,最终还是会回归到 Promise 的错误处理机制上,即通过 .catch()
来处理未被捕获的拒绝。这其实提供了一个非常灵活的错误处理策略:你可以在函数内部进行细粒度的错误处理,也可以将错误冒泡到调用栈的更高层级进行统一处理。
如何优化 async
函数以提高性能?并行执行与串行执行的选择。
优化 async
函数的性能,很多时候归结为一句话:在不影响逻辑依赖的前提下,尽可能地并行执行独立的异步操作。 async/await
默认是串行执行的,这在处理有前后依赖关系的任务时非常直观和安全,但如果任务之间没有依赖,串行执行就会白白浪费时间。
串行执行 (默认行为)
当你连续
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。
这种模式适合有依赖关系的任务流,代码逻辑清晰,易于理解。
并行执行 (
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
函数性能优化的一个核心策略。并行执行 (
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 状态和结果的数组。这在你想知道所有操作的结果,即使某些失败了也想继续处理时非常有用。
避免不必要的
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学习网公众号,带你了解更多关于的知识点!
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
159 收藏
-
248 收藏
-
367 收藏
-
125 收藏
-
427 收藏
-
103 收藏
-
432 收藏
-
391 收藏
-
361 收藏
-
165 收藏
-
125 收藏
-
213 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 511次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 498次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习