想象你走进一家餐厅,点了一份招牌菜。服务员没有让你站在柜台前干等,而是递给你一张取号单:“您先回座位,菜好了我叫您。”这张取号单,就是 JavaScript 里的 Promise。它在手里时是“等待中”,菜端上桌是“成功”,厨房告诉你做不了是“失败”。你可以在这张单子上挂几个叮嘱:“菜好了叫我”“失败了也通知我”“不管怎样吃完再买单”——这就是 Promise 的链式能力。后来餐厅升级了服务,你可以直接“在座位上等上菜”,代码像看菜单一样从上往下读——这就是 async/await。本文用这个“餐厅点餐”的比喻,带你彻底搞懂异步编程的现代套路。
对 AI 从业者:无论做前端可视化、模型接口调用还是数据管道,异步编程都是让 AI 应用“不卡顿”的基石。掌握 Promise 与 async/await,你写出的代码会更像一条流畅的服务流水线,而不是一团乱麻。
可视化演示
展示Promise三种状态流转、then/catch/finally链式调用机制,以及Promise.all并行与await串行执行的时间对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Promise三种状态流转 Pending:初始状态,等待异步操作完成 Fulfilled:操作成功,状态不可逆 Pending → Fulfilled | Rejected 状态一旦确定不可更改,这是Promise可靠性的基础
三种状态
当你拿到取号单时,它只是一张薄薄的纸,上面没有菜,只有一排号码。这张单子此刻处于“等待中”——厨房还在忙活。过了一会儿,服务员端着热腾腾的菜走来,单子的使命完成了,它变成“成功”;如果厨师告诉你“今天的鱼卖完了”,单子就变成“失败”。这就是 Promise 的三种状态:pending、fulfilled、rejected。
在 JavaScript 中,一个 Promise 对象从创建那一刻起,就必然处于这三种状态之一。你通过 new Promise((resolve, reject) => {...}) 创建它,executor 中的异步任务完成时调用 resolve(value),失败时调用 reject(reason)。这三个状态不是装饰,而是异步结果的形式化表达:pending 表示结果未知;fulfilled 表示已成功拿到结果,附带一个值 value;rejected 表示失败,附带一个原因 reason。
特别强调:状态一旦从 pending 变成 fulfilled 或 rejected,就永久锁定,后续再调用 resolve 或 reject 都会被忽略。这就像取号单一旦变成“成功”,服务员绝不会再把它改成“失败”——菜都上桌了,怎么可能又撤回?所以 Promise 的状态不可逆,这是它可靠性的基石。
回到餐厅场景,你手中的取号单永远只会命中一种结局:要么等来菜,要么等来“卖完了”,没有第三种可能。理解这一点,你就抓住了 Promise 的核心。
- 等待态起始
\text{Pending}表示 Promise 刚创建时的等待状态,相当于取号单刚打印出来,厨房还没出菜。 - 成功转移箭头
\xrightarrow{\text{resolve(value)}}表示调用 resolve 并携带一个值 value,状态转向 Fulfilled。 - 失败转移
\text{reject(reason)}表示调用 reject 并携带失败原因 reason,状态转向 Rejected。 - 终点状态
\text{Fulfilled}与\text{Rejected}是两个吸收态,一旦到达就固定,任何后续操作都无法改变。
状态流转
取号单不会自己变成“成功”或“失败”,是后厨的动作决定了它的命运。厨师做好菜,喊一声“32号!”,单子就变成了“成功”;厨师发现食材不够,在单子上画个叉,它就变成了“失败”。在 Promise 的世界里,这两个“喊话”就是 resolve 和 reject。
resolve 和 reject 是 executor 中两个函数参数。resolve(value) 将 Promise 推向 fulfilled,且把 value 传递给后续的 then 回调;reject(reason) 将 Promise 推向 rejected,并把 reason 传递给后续的 catch 回调。一旦调用其中一个,Promise 的状态就立刻锁定,另一个调用会被忽略,这就是“一次性转变”。
这里有个容易忽略的细节:value 和 reason 可以是任何 JavaScript 值。value 常是接口返回的数据,reason 常是 Error 对象。它们被“存”在 Promise 内部,等待消费者(then/catch)来取。你可以把 Promise 想象成一个带锁的盒子,resolve 存进成功结果,reject 存进失败原因,锁上之后只有对应的钥匙能打开。
你手中那张取号单,最终只能被服务员喊一次号,或者被划掉一次。Promise 的 resolve/reject 也遵循同样的规则——先到先得,后到无效。
- 状态函数
\text{state}(\text{Promise})表示我们观察一个 Promise 当前处于什么状态,就像看取号单上的标记。 - 成功分支
\text{fulfilled}(v)表示成功状态携带一个值 v,这个值会传给 then 中的回调。 - 失败分支
\text{rejected}(r)表示失败状态携带一个原因 r,这个原因会传给 catch 中的回调。 - 未决分支
\text{pending}表示还未分晓,相当于后厨还在忙,没有喊号也没有画叉。
链式调用
以前你想知道菜好了没,得守在窗口一遍遍问。现在取号单让你把“叮嘱”写成一串:先等菜好,菜好了加个米饭,米饭上了再结账。Promise 的 then/catch/finally 就是这种穿成串的叮嘱。
promise.then(onFulfilled, onRejected) 用于注册成功或失败的回调;catch(onRejected) 专门处理失败;finally(onFinally) 无论成功失败都会执行。关键特性是:then 和 catch 都会返回一个新的 Promise,所以可以无限链接下去。每个 then 中 return 的值,会成为下一个 then 收到的参数,这就是“返回值透传”。
比如 p.then(f).then(g),如果 f 返回一个新 Promise,g 会等待它完成后再执行。这就像你去餐厅:先等 A 菜上桌,再让服务员去催 B 菜,最后等 B 菜上桌再动筷子。每一步都清清楚楚,不会挤在一起。
链式调用让异步流程从“层层嵌套”变成“一条直线”。你只需要像写购物清单一样,把每一步要做的事写在 then 里,代码自然就流畅了。
- 起点 Promise
p表示初始的 Promise 对象,相当于第一张取号单。 - 第一次挂单
p.\text{then}(f)在 p 上注册回调 f,等待 p 的结果。 - 第二次挂单
.then(g)在前一个 then 返回的新 Promise 上再注册 g,形成串联。 - 等价组合
x \mapsto g(f(x))表示把 f 和 g 组合成一个函数,先执行 f 再执行 g。这对应“等 f 的结果出来后,把结果交给 g”。
语法糖原理
现在你知道了 Promise 链式调用很好用,但写多了还是会觉得有点啰嗦:每个 then 都要包一层回调函数。于是餐厅推出了“VIP 座位”——你直接坐下来等,服务员上菜到你面前。这就是 async/await。
async 关键字声明一个异步函数,这个函数总是返回一个 Promise。await 关键字放在一个 Promise 前面,会“暂停”当前 async 函数的执行,直到这个 Promise settle。暂停期间 JavaScript 主线程并没有闲着,它继续处理其他任务;等 Promise 结果出来后,函数再自动恢复执行。
本质上,await 就是 then 回调的语法糖。const data = await fetch(url) 等价于 fetch(url).then(data => {...}),只是把“回调里的代码”重新组织成了“从上到下的顺序代码”。这让异步代码读起来和同步代码几乎一样,大大降低了心智负担。
坐在餐厅的 VIP 座位上,你不需要举着取号单反复核对,服务员会主动把菜端到你面前。async/await 就是这样一种“不打断思路”的写法,但它背后依然是那张取号单——Promise。
- 等待指令
\langle \text{await}\ p; \ \text{next} \rangle表示程序执行到 await 时暂停,等待 p 完成后再执行下一步 next。 - 等价转换
\leadsto表示“可以等价变形为”,就像把一种写法翻译成另一种写法。 - 回调写法
p.\text{then}(\text{next})表示在 p 成功后的回调里执行 next,这是没有 await 时的传统写法。
错误处理
就算取号单再方便,菜也可能做不出来。最怕的是服务员把“做不了”的消息藏在角落,你等了一小时才发现。Promise 的 catch 和 async/await 的 try/catch,就是专门负责把失败原因及时送到你耳边。
在 Promise 链中,任何一个环节抛出的错误,都会被后面的 catch 捕获。这就像传菜过程中,任何一桌出了问题,服务员都会通过对讲机通知领班。在 async/await 中,你只需要把 await 语句包在 try/catch 里,错误就自动变成 catch 的参数。给 throw 一个值,或者 await 一个 rejected 的 Promise,都会触发这个机制。
需要注意的是,如果错误没有 catch 处理,就会变成“未捕获的 Promise 异常”,可能导致程序崩溃或难以排查的现象。这就像服务员喊了三声没人应,最后只能把菜倒掉,问题被无声吞没。finally 则负责“总会执行”的收尾工作,比如关闭加载动画、释放资源。
错误处理就像餐厅的应急预案:菜做砸了,不仅要说“抱歉”,还要告诉你原因、给你一个补偿方案。Promise 的 catch 就是那个主动道歉的服务员,而 try/catch 则让你能安心坐着,把“意外”交给兜底机制。
- 抛错动作
\text{throw}\ e表示代码主动抛出一个错误 e,比如校验失败时触发。 - 等价符号
\equiv表示两者在 Promise 错误处理机制中的行为完全一致。 - 拒绝调用
\text{reject}(e)表示把 Promise 推向 rejected 状态,并携带错误 e 作为原因。
并行串行
你请朋友吃饭,一口气点了五道菜。如果让厨房一道一道做,等最后一个菜上桌前面的早就凉了。高效的方式是:五道菜同时下锅,全部上齐再开席。这就是 Promise.all 的并行思维。而有些菜必须按顺序吃,比如先喝汤再吃主菜,这就是串行控制。
Promise.all([p1, p2, p3]) 接收一个 Promise 数组,返回一个新 Promise。只有数组里所有 Promise 都 fulfilled,它才 fulfilled,结果是一个对应顺序的数组;只要有一个 rejected,它就立刻 rejected。这就像你点了五道菜,服务员必须等全部出锅才一起端上来,有一道做砸了,整桌都受影响。
Promise.race([p1, p2, p3]) 则相反,谁先 settle 就用谁的结果。这像你同时叫了三家外卖,谁先送到吃谁的,其余的直接取消。串行控制则需要用 for 循环配合 await 逐条处理,或者用 then 链一层层连接,确保前一个完成后再启动下一个。
把异步任务想象成餐厅的出菜流程:需要同时上的菜就用 Promise.all,需要比速度的就用 Promise.race,必须一碟一碟上的就用串行控制。选对组合方式,才能让整个流程既高效又稳定。
- 并行数组
[p_1,\dots,p_n]表示一个包含 n 个 Promise 的数组,相当于 n 张取号单同时发出去。 - 整体成功
= \text{fulfilled}([v_1,\dots,v_n])表示当所有 Promise 都成功时,整体进入 fulfilled,并携带每个结果值组成的数组。 - 条件判断
\quad \text{若所有 } p_i \text{ 都成功}是数学里的条件说明,强调这是“全员通过”才成立的结果。
回调地狱
在 Promise 出现之前,异步代码长什么样?想象你去一家没有取号单的老餐厅,想点三个菜,服务员说:“你先等第一个菜,第一个菜好了,我再帮你去问第二个菜,第二个菜好了,我再帮你问第三个菜。”于是你只能坐在厨房门口,手里攥着三张写着上个服务员电话的纸条,一层套一层,看都看不懂。
这种“回调里套回调”的写法被称为回调地狱。每一层缩进都代表一个异步依赖,错误处理更是噩梦:每个回调都得单独判断有没有出错,稍不留神就给漏了。代码的可维护性极差,改一个逻辑要顺着嵌套往里钻。
Promise 的链式调用把嵌套拍平了。每一层 then 都在同一缩进级别上,错误统一交给 catch,流程一目了然。async/await 则更进一步,用同步语法的外壳包裹异步逻辑,让代码读起来就像菜谱上的步骤清单。从回调地狱到 Promise,再到 async/await,核心思路始终是:把“先后顺序”显式地表达出来,而不是藏在嵌套里。
餐厅升级了取号系统后,你再也不用攥着一把纸条挤在厨房门口。Promise 和 async/await 就是这套现代取号系统,它们让异步代码从“迷宮”变成了“直线”。
---
- 嵌套表达式
f(g(h(x)))表示三个异步操作层层包裹,必须从最内层 h 开始执行。 - 标注
\underbrace{\dots}_{\text{嵌套回调}}用花括号和文字标出这是造成阅读困难的嵌套结构。 - 映射箭头
\mapsto表示从旧写法转换为新写法。 - 链式表达
f.\text{then}(g).\text{then}(h)表示把三个操作按顺序平铺,每个 then 只依赖前一个结果。
AI 应用场景
前端可视化数据加载
阶段:数据接入与展示
解决的问题:AI 模型推理通常需要几百毫秒甚至几秒,如果同步等待,页面会卡死,用户体验极差。传统回调方案容易把加载逻辑和 UI 更新混在一起,难以维护。
具体案例:一个 AI 图像识别页面,点击“识别”按钮后,前端用 async/await 调用后端推理接口。await 等待期间页面显示加载动画;拿到结果后用 try/catch 捕获接口异常,最后用 finally 关闭加载动画,再更新图表展示识别结果。
并行调用多个模型服务
阶段:模型推理加速
解决的问题:一次智能问答可能需要同时调用语音识别、意图分类、知识检索三个服务。串行调用会累加延迟,用 Promise.all 并行发起请求能把总耗时降到最慢的那个服务耗时。
具体案例:智能客服机器人接收用户语音后,前端并行调用三个 AI 服务:语音转文字、情感分析、商品推荐。Promise.all 等待三个结果都返回后,统一渲染答案卡片。如果某个服务超时,用 Promise.race 设置超时兜底,避免用户无限等待。
数据管道串行处理
阶段:数据预处理
解决的问题:清洗和转换数据时,某些操作必须严格按顺序执行,比如先翻译再摘要,不能并行。用 async/await 配合 for 循环可以逐条处理,同时保持代码清晰。
具体案例:批量翻译场景中,先对每篇文章调用翻译服务,翻译完成后再调用摘要服务,最后写入数据库。用 for...of 循环配合 await 逐篇处理,避免同时发起太多请求导致服务限流。
Python 演示
class SimplePromise: def __init__(self, executor): self._state = 'pending' self._value = None self._handlers = [] try: executor(self._resolve, self._reject) except Exception as exc: self._reject(exc) def _resolve(self, value): if self._state != 'pending': return self._state = 'fulfilled' self._value = value self._flush() def _reject(self, reason): if self._state != 'pending': return self._state = 'rejected' self._value = reason self._flush() def _flush(self): while self._handlers: handler = self._handlers.pop(0) self._invoke(handler) def _invoke(self, handler): on_fulfilled, on_rejected, resolve, reject = handler try: if self._state == 'fulfilled': if on_fulfilled is not None: result = on_fulfilled(self._value) else: result = self._value else: if on_rejected is not None: result = on_rejected(self._value) else: reject(self._value) return resolve(result) except Exception as exc: reject(exc) def then(self, on_fulfilled=None, on_rejected=None): def executor(resolve, reject): self._handlers.append((on_fulfilled, on_rejected, resolve, reject)) if self._state != 'pending': self._flush() return SimplePromise(executor) def catch(self, on_rejected): return self.then(None, on_rejected) def finally(self, on_done): def passthrough(value): on_done() return value def rethrow(reason): on_done() raise reason return self.then(passthrough, rethrow) if __name__ == '__main__': def async_task(name): print('开始处理:', name) return SimplePromise(lambda resolve, reject: resolve(name + '完成')) p = async_task('取号单A') p.then(lambda v: print('第一个then:', v)).then(lambda v: print('第二个then:', v)) p.catch(lambda e: print('捕获错误:', e))
self._state = 'pending'初始化 Promise 状态为等待中,相当于取号单刚打印出来。executor(self._resolve, self._reject)立即执行传入的函数,并把两个内部方法交给它,模拟 JavaScript 的 executor。if self._state != 'pending': return状态一旦改变,后续 resolve/reject 调用全部忽略,保证不可逆性。self._handlers.append(...)把 then 注册的回调和新的 resolve/reject 绑定起来,形成链式调用的基础。return SimplePromise(executor)then 返回一个新的 Promise,让调用可以继续挂下去。_flush()状态 settle 后,依次执行所有已注册的处理器,相当于服务员开始叫号。rethrow(reason): on_done(); raise reasonfinally 中如果原来的 Promise 失败,执行完清理后继续抛出错误,不吞掉异常。
常见误区
误区我以为 `resolve()` 之后如果代码又执行了 `reject()`,Promise 的状态会变成 rejected,或者后面的调用会覆盖前面的结果。总之状态是可以改的。
正解Promise 的状态只能从 pending 变为 fulfilled 或 rejected,且只能变一次。一旦状态变为 fulfilled,再调用 reject 会被静默忽略,反之亦然。这就像取号单已经被服务员喊了“32号上菜”,之后就算厨师说鱼卖完了,菜也照样端上桌。
为何常见:因为普通变量可以反复赋值,人们容易把 Promise 也当成一个“可改写的容器”,忽略了它设计上的不可逆性。
误区我以为 `await` 会像 Python 的 `time.sleep()` 一样,把整个主线程卡住,等到异步操作完成才继续执行。
正解await 只暂停当前 async 函数内部后续代码的执行,不会阻塞 JavaScript 主线程。主线程依然可以响应点击、渲染动画。这就像你在座位上等菜,厨房里其他菜照常做,其他客人照常吃。
为何常见:因为代码看起来像同步的,初学者容易从“视觉上的同步”推出“实际上的阻塞”,忽略了事件循环的存在。
误区我以为 `finally` 一定会等到 `catch` 执行完之后才执行,所以如果 `catch` 里又抛了错,`finally` 就不跑了。
正解finally 无论 Promise 成功还是失败都会执行,而且如果 catch 中再次抛错,finally 依然会执行,之后新错误继续向外传播。它就像餐厅的“买单”环节,不管菜好不好吃,有没有投诉,临走前都要买单。
为何常见:很多人按字面理解“catch 处理错误,finally 作收尾”,却忽略了 finally 的“无论怎样都执行”这个更强语义。
误区我以为 `Promise.all` 会等数组里所有 Promise 都结束,哪怕其中一个已经 rejected,也要等全部完成再统一报错。
正解Promise.all 是“快速失败”的:只要有一个 Promise rejected,它立刻返回一个 rejected 的新 Promise,不会等待其余 Promise 结束。这就像一桌菜里有一道做砸了,服务员马上告诉你整桌预订取消,剩余几道菜不用等了。
为何常见:名字里带“all”,人们容易联想到“全部完成才处理”,忽略了它内部实现时的短路机制。
面试题
Q1. 一个 Promise 刚被创建时,它的状态是?
- A.等待中(pending)← 正确答案
- B.成功(fulfilled)
- C.失败(rejected)
- D.不确定状态
答案:A。Promise 创建后立即进入 pending 状态,就像取号单刚打印出来,还没被服务员喊号。只有调用 resolve 或 reject 后才会进入后续状态。
Q2. Promise 状态从 pending 变为 fulfilled 后,再次调用 resolve() 会怎样?
- A.再次触发 then 回调
- B.被静默忽略← 正确答案
- C.抛出运行时错误
- D.状态变成失败
答案:B。Promise 状态一旦 settle 就不可变,后续 resolve 或 reject 调用都会失效。取号单已经被喊过一次号,再喊多少次也没用。
Q3. 执行 promise.then(f).catch(g).finally(h),若 f 抛出异常,哪些回调会执行?
- A.只有 f 会执行
- B.只有 g 会执行
- C.g 和 h 都会执行← 正确答案
- D.只有 h 会执行
答案:C。f 抛错后,链上的 catch 会捕获这个错误,所以 g 执行;finally 无论成功失败都会执行,所以 h 也执行。这像菜做砸了,服务员来道歉(g),然后你还是要买单(h)。
Q4. 一个 async 函数的返回值默认会被包装成什么?
- A.一个 Promise 对象← 正确答案
- B.普通原始值
- C.null 值
- D.undefined 值
答案:A。async 函数总是返回 Promise,即使函数内部只返回一个普通值,也会自动包裹成 fulfilled 的 Promise。这相当于不管你要不要,餐厅都会给你一张取号单作为凭证。
Q5. 在 async 函数中 await 一个已经 fulfilled 的 Promise,会发生什么?
- A.阻塞整个 JS 线程
- B.暂停当前 async 函数← 正确答案
- C.立即抛出异常
- D.跳过 await 继续执行
答案:B。await 会暂停当前 async 函数,把后续代码放到微任务队列里,即使 Promise 已成功也一样要等下一个事件循环周期。JS 主线程不会阻塞,其他任务照常运行。
Q6. 关于 Promise.all 的特点,下列哪些说法正确?
- A.全部成功则整体成功← 正确答案
- B.任一失败则快速失败← 正确答案
- C.同时等待所有 Promise← 正确答案
- D.由最先完成者决定结果
答案:ABC。Promise.all 会等待所有 Promise 完成,全部成功时返回结果数组;任何一个失败会立刻短路失败。D 描述的是 Promise.race 的行为,不是 all。
Q7. 关于 finally,下列说法正确的是?
- A.无论成功失败都会执行← 正确答案
- B.能够覆盖链式返回的值
- C.原状态和值会被保留← 正确答案
- D.会自动吞掉错误
答案:AC。finally 的回调不接收结果,返回值也会被忽略,所以不能覆盖值和状态;错误会继续向后传播,不会被吞掉。它像“买单”动作,不影响菜的评价。
Q8. catch(onRejected) 等价于 then 的哪个用法?
- A.第一个参数传 onRejected
- B.第二个参数传 onRejected← 正确答案
- C.第三个参数传 onRejected
- D.与 then 完全无关系
答案:B。then 接受两个参数,第二个参数就是 rejected 时的回调。catch 是 then(undefined, onRejected) 的简写,就像把失败处理单独抽出来。
Q9. 串行控制多个异步任务时,哪些做法是正确的?
- A.在 then 中返回新 Promise← 正确答案
- B.用 for 循环配合 await← 正确答案
- C.用 Promise.all 全部并行
- D.在回调中继续嵌套下一层
答案:AB。A 和 B 都能保证前一个任务完成后才启动下一个,是串行控制。C 是并行,不满足串行;D 会形成回调地狱,虽然能跑但极难维护。
Q10. 一个 async 函数内部主动 throw 一个错误,调用该函数会返回什么?
- A.一个 rejected 的 Promise← 正确答案
- B.一个 fulfilled 的 Promise
- C.直接抛出同步错误
- D.返回 undefined 值
答案:A。async 函数中的 throw 会被引擎捕获,并自动转换成 rejected 的 Promise,而不是同步抛错。这相当于厨房把“做不了”划在取号单上,而不是直接把单子扔到你脸上。