想象你走进一家智能工厂,一条长长的传送带把半成品依次送到各个工位:第一个工位扫码登记,第二个工位组装零件,第三个工位喷漆,第四个工位质检。每个工位只干一件事,干完后把半成品放到传送带,交给下一个工位。Promise 链式调用就是前端世界里的这条传送带——一段异步任务完成后,把结果“放上履带”传给下一段,再传给下一段,直到整条流水线完工。普通回调函数就像每个工人都要跑完整个车间,而链式调用让每一步都分工明确、互不阻塞。本文会用这条“异步流水线”的比喻,把 then 的返回值传递、普通值与 Promise 的区别、错误冒泡、Promise.all / Promise.race、串行执行和常见陷阱全部讲透。看完你就能像设计工厂动线一样,优雅地编排任何复杂异步流程。

岗位关联

学会 Promise 链式调用,你就能把散落一地的嵌套回调改写为可读、可维护、可调试的流水线代码——这是前端工程化和 AI 应用前端开发中绕不开的基本功。

目录
  1. 可视化演示
  2. then返回值传递
  3. 状态流转规则
  4. 错误冒泡机制
  5. 并行控制
  6. 竞速控制
  7. 串行执行
  8. 常见陷阱
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示冒泡排序的逐轮比较与交换过程,直观呈现大元素逐步冒泡到数组末尾的排序机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

冒泡排序 - 第1轮 比较相邻元素大小交换位置 大元素逐步冒泡到末尾 a[j]>a[j+1] 相邻比较交换

then返回值传递

流水线第一站,工人们最常犯的困惑是:“下一个工位到底拿到的是什么?”答案藏在这条铁律里:下一个 then 的输入,永远是上一个 then 的返回值。就像传送带不会自己造零件,它只负责把上游工位放上来的半成品原样送下去。如果第一个 Promise resolve 出 "零件A",第一个 then 加工后 return "零件A已打孔",那么第二个 then 拿到的就是字符串 "零件A已打孔"。

技术上说,then 方法会创建并返回一个新的 Promise,这个新 Promise 的 resolve 值由回调函数的返回值决定。于是 p.then(f).then(g) 中,g 拿到的参数是 f(v) 的结果。这一步是整条链的地基:只要遵守“每步都 return”,链条就能想接多长接多长。

回到工厂:你只需要保证每个工人在完工后把产品放上传送带(return),下游就永远知道自己该加工什么。忘记放上去,下游只能对着空气干活——拿到一个 undefined。

resolved(v)→ then(f) resolved(f(v))\displaystyle \text{resolved}(v) \xrightarrow{\ \text{then}(f)\ } \text{resolved}(f(v))
这个式子说明:传送带上一站的成品值 v,经过工位 f 加工后,变成新的成品值 f(v) 继续流动。
  1. resolved(v)v 表示上一层成功产出的值,resolved 表示该值已经被 Promise 确认,相当于质检合格的半成品。
    vv
  2. then(f)f 是当前工位的加工函数,then(f) 就是把 f 挂到这条传送带上。
    ff
  3. f(v)f 接过 v 进行加工,返回的就是新半成品,这是“上一站的结果传给下一站”的关键一步。
    f(v)f(v)
  4. resolved(f(v))f(v) 被 Promise 再次包装成成功状态,作为下一站的输入。
    resolved(f(v))\text{resolved}(f(v))

状态流转规则

现在工位前出现一个岔路口:工人加工完,可能放回的是一件普通成品,也可能是“装着另一条流水线的委托单”。两者在传送带上的待遇完全不同。如果你返回一个普通值(数字、字符串、对象),Promise 会立刻把它包装成成功状态,半成品“刷”一下就到了下一站;如果你返回的是一个 Promise,传送带会卡在原地,直到那个内层 Promise 尘埃落定,再把它的最终结果传下去。

这个机制正是 Promise 链优雅的关键:自动扁平化。不管 f 返回的是裸值还是 Promise,下游 then 拿到的永远是“解开包装后的最终值”,而不是“一个 Promise 套 Promise 的俄罗斯套娃”。用代码体会一下:p.then(() => fetch('/api')).then(res => res.json()),你不需要在第二个 then 里再调用 .then 去解包 fetch 返回的 Promise,引擎帮你干了这件事。

工厂里的老师傅会告诉你:拿到委托单(Promise)别拆开,直接原封不动传上去,上层传送带会自动等委托单对应的车间完工。你越是想“手动拆单”,链条就越臃肿。

return(v)⟶{fulfilled(v),v 是普通值Pnew,v 是 Promise,等待其 settle\displaystyle \text{return}(v) \longrightarrow\begin{cases}\text{fulfilled}(v), & v \text{ 是普通值} \\[4pt]P_{\text{new}}, & v \text{ 是 Promise,等待其 settle}\end{cases}
引擎拿到回调返回值后会"体检":是普通值就直接进入成功态;是新 Promise 就原地等候,直到它给出最终裁决。
  1. return(v)这是 then 回调的产出,也是链上唯一要紧的出口。
    vv
  2. fulfilled(v)普通值 v 被自动包装为成功态,立即传给下一个 then。
    fulfilled(v)\text{fulfilled}(v)
  3. P\_new如果 v 本身是 Promise,则外层 Promise 直接接管它的命运,等它 settle 后继续。
    PnewP_{\text{new}}
  4. 普通值 / Promise 分岔用 case 环境表达“体检分拣”的逻辑,两个分支代表引擎自动做出的决定。
    {fulfilled(v)Pnew\begin{cases} \text{fulfilled}(v) \\ P_{\text{new}} \end{cases}

错误冒泡机制

流水线上难免出次品。Promise 链的质检规则很简单:任何一环抛出错误或 reject,后面的所有成功工位(then 的第一参数)全部跳过,错误自动“冒泡”向下游滚动,直到撞见第一个 catch。就像传送带上的红色警报一旦拉响,沿途工位全部停工让行,直到回收站(catch)把次品接走。

写代码时,catch 本质是 then(null, onRejected) 的语法糖。它的位置决定了作用范围:放在链尾,可以兜住整条链上任何一环的错误,是“安全网”;放在中间,则相当于“就地修复站”,catch 里 return 一个新值后,链条还能继续往下走。记住:catch 不是终点站,只是回收站,处理完照样可以重新上线。

真实项目里,老练的开发者会刻意在链尾挂一个 catch,让流水线任何一处爆炸都不至于变成无人处理的“野次品”。而中间 catch 则像产线上的返工台,把错误消化掉,放回一个可用的半成品让链条继续跑。

rejected(e)⇒skip(then1,then2,…)⇒catch(e)\displaystyle \text{rejected}(e) \Rightarrow \text{skip}(\text{then}_1, \text{then}_2, \ldots) \Rightarrow \text{catch}(e)
错误 e 一出现,中间所有成功回调立刻失效,e 顺着链条直达最近的 catch,就像警报让所有普通工位暂停、直接把次品送去回收站。
  1. rejected(e)链条中某一步 reject 了,e 就是要传递的错误对象。
    ee
  2. skip(……)引擎跳过中间所有 onFulfilled 回调,它们没有机会被执行。
    skip(then1,then2)\text{skip}(\text{then}_1, \text{then}_2)
  3. catch(e)错误在第一个 catch 处着陆,你可以在那里记录日志、提示用户或恢复。
    catch(e)\text{catch}(e)

并行控制

有些任务彼此毫无依赖——比如同时请求三个不相关的接口。如果硬排成串行,就是在浪费传送带。此时该让多条支线同时开工,再汇聚到同一个合流口。Promise.all 就是这个合流口:它接收一个 Promise 数组,同时启动所有任务,等全部成功后,把每个结果按原始顺序装进一个数组交给你;只要有一个失败,整个 all 立刻进入 rejected 状态。

注意两个容易忽略的细节:一是结果的顺序与数组传入顺序一致,而不是按完成速度排序,就像流水线合流时每个工位按编号入列;二是“快速失败”——某个任务提前 reject,all 不会再等其他任务,马上报错。所以它适合“全部成功才算成功”的场景:比如批量上传图片、并行拉取首页各模块数据。

工厂里最典型的用法是“并行组装再总装”:几个车间各自干活,互不通气,最后传送带把所有人成果汇成一个订单包。这正是 Promise.all 的写照——并发不是玄学,只是把该同时干的事同时铺开。

Pall={fulfilled([v1,v2,…,vn]),所有 Pi 成功rejected(e),存在 Pi 失败\displaystyle P_{\text{all}} =\begin{cases}\text{fulfilled}([v_1, v_2, \ldots, v_n]), & \text{所有 } P_i \text{ 成功} \\[4pt]\text{rejected}(e), & \text{存在 } P_i \text{ 失败}\end{cases}
all 的裁决是"一票否决制":全体通过才发通行证,任何一环亮红灯整个批次立刻叫停。
  1. P\_i数组里的第 i 个 Promise 任务,代表一条独立支线。
    PiP_i
  2. fulfilled([\dots])全部分支成功,结果按传入顺序聚合成数组。
    [v1,v2,…,vn][v_1, v_2, \ldots, v_n]
  3. rejected(e)只要有一支失败,整体立即失败,e 就是最先失败的那个原因。
    rejected(e)\text{rejected}(e)
  4. 大括号分情况用 cases 表达“全通过 / 有失败”的两种结局。
    {fulfilled([vi])rejected(e)\begin{cases} \text{fulfilled}([v_i]) \\ \text{rejected}(e) \end{cases}

竞速控制

传送带上还有一种特殊装置:多路原料同时从不同门进入,谁最先抵达主传送带,就用谁的结果。Promise.race 干的就是这件事——它同样接收一个 Promise 数组,但只关心第一个 settle(成功或失败都算)的任务。注意:是“settle 先到先得”,而不是“成功先到先得”。如果最快的那个任务 reject 了,race 的结果就是失败。

这个特性催生了最经典的超时兜底模式:Promise.race([真实请求, 定时器reject])。真实请求 3 秒没回,就先让 2 秒后 reject 的定时器赢下比赛,从而把“卡死的请求”降级为“超时错误”。在 AI 应用里,这常用于大模型流式响应的超时保护——宁可快速失败重试,也好过让用户无限转圈。

工厂里的“加急通道”就是这样:几个供应商同时发货,谁先送到就用谁的货。超时兜底则是往赛道里塞一个“准时炸弹”——2 秒没到货就自动判定超时,宁可触发应急方案也不傻等。

Prace=Pk,其中 tk=min⁡{t1,t2,…,tn}\displaystyle P_{\text{race}} = P_k, \quad \text{其中 } t_k = \min\{t_1, t_2, \ldots, t_n\}
race 的规则朴素得像一场赛跑:不看实力只看计时器,最先撞线的那个对象——不论它手里拿的是合格品还是次品——决定整场比赛的结局。
  1. P\_k第 k 个任务成了赢家,race 返回的 Promise 状态与它完全一致。
    PkP_k
  2. min{t\_i}t\_i 是第 i 个任务 settle 所花的时间,取最小值就是“最快完成”。
    min⁡{t1,t2,…,tn}\min\{t_1, t_2, \ldots, t_n\}
  3. t\_k = ……等式表达“选中的是耗时最短的那个”,胜出者只有一个。
    tk=min⁡{ti}t_k = \min\{t_i\}

串行执行

与并行相对的场景是:任务之间有着严格的先后依赖——取 token、再拿用户信息、再查权限、最后加载首页。这时不能并行,必须让上一步的结果作为下一步的输入,一条道走到黑。串行执行就是手动把 Promise 一条一条接起来,常见三种写法:for……of 循环逐个 await、reduce 把任务折叠成链、递归每次推进一步。

以 for……of 为例,你可以在循环体内 await 一个 Promise,把结果存进数组,再进入下一轮——每一步都站在前一步肩膀上。用 reduce 更优雅:tasks.reduce((chain, task) => chain.then(task), Promise.resolve()),它把一组任务像链条焊接一样连起来,初始值就是空转的 Promise.resolve()。数组有多少个任务,链就有多少节。

串行像工厂里一道严格的工艺顺序:淬火不能先于锻造。轮到你写 AI 请求管线时,先用 all 把没依赖的请求并发出去,再用串行链处理依赖步骤,流水线就算设计清楚了。

R=P0→ f1 P1→ f2 ⋯→ fn Pn\displaystyle R = P_0 \xrightarrow{\ f_1\ } P_1 \xrightarrow{\ f_2\ } \cdots \xrightarrow{\ f_n\ } P_n
串行链的数学形象就是一条单行道:P0 出发,每个 f 依次加工,最终产出 Pn;途中断一节,后面全部停摆。
  1. P\_0串行的起点,通常来自 Promise.resolve() 或第一个真实任务。
    P0P_0
  2. f\_i第 i 个加工函数,接收上一步结果,返回新 Promise。
    fif_i
  3. P\_i经过 f\_i 后形成的中间 Promise,它同时是下一步的输入。
    PiP_i
  4. 箭头链\xrightarrow{f_i} 表示 f\_i 负责把 P\_{i-1} 递进到 P\_i,强调顺序不可颠倒。
    Pi−1→fiPiP_{i-1} \xrightarrow{f_i} P_i

常见陷阱

流水线装得再漂亮,也怕几个“低级事故”。这条链上最经典的事故是忘记 return:在 then 里做了一堆操作却忘了把结果放回传送带,下一个 then 只能收到 undefined。第二步的输入莫名其秒变成空值,调试时能追到怀疑人生。其次是在 then 里嵌套新的 Promise 链而不是把它 return 出去,这会让扁平的流水线重新长成回调地狱那棵歪脖子树。

还有一个隐蔽问题:catch 的位置放错。有人觉得“反正 catch 能捞错,放哪都行”,却忘了 catch 放在中间会在接住错误后让链继续走,放在链尾则拦住整条链的所有错误。如果中间的某个 then 才是你认为的“安全阀”,链尾 catch 可能永远接不到预期的错误。最后,误把 Promise.all 当串行、误把 race 当“只取成功”,也都源于没吃透两种聚合语义。

工厂最怕的不是机器故障,而是工人忘了把产品放上传送带——机器空转,下游饿肚子。每条 then 里写代码时,先问自己一句:我 return 了吗?

then(f) 没有 return f(v)⇏fulfilled(f(v))\displaystyle \text{then}(f) \ \text{没有 return } f(v) \quad \not\Rightarrow \quad \text{fulfilled}(f(v))
这条式子直接戳破陷阱:不 return,下游收到的就不是加工值 f(v),而是被当作 undefined 的失落半成品。
  1. then(f)对当前 Promise 挂上回调 f,期待它产出新值。
    then(f)\text{then}(f)
  2. 没有 returnf 内部执行了逻辑但没返回值,引擎只能把 undefined 当作结果。
    undefined\text{undefined}
  3. \not\Rightarrow fulfilled(f(v))箭头被打叉,意思是“期待中的传递根本不会发生”,链在此处静默断裂。
    fulfilled(f(v))\text{fulfilled}(f(v))

AI 应用场景

AI 助手请求编排(串行链)

阶段:前端交互编排 / 请求管线设计

解决的问题:AI 助手聊天页需要先拿 token、再携带 token 调 LLM 接口、然后解析流式响应最后更新 UI。传统做法是四层嵌套回调,状态一多就崩;用 Promise 链把每步串成一个流水线,每步输出喂给下一步,任何一步出错都汇入统一的链尾 catch,代码像读故事一样清晰。

具体案例:某 AI 写作助手前端,用户在输入框点击“生成”,前端先 fetch('/api/token'),再 fetch('/api/llm', {headers: {Authorization}}),最后用 streamReader 读取流式数据逐段渲染。三步连成一条 then 链,超时和 token 过期都交给链尾 catch 统一处理。

批量向量化的并行控制

阶段:数据预处理 / 批量推理调度

解决的问题:把一篇长文档切成 50 个片段做 embedding,如果一个个串行请求要等 50 次往返。用 Promise.all 一次并发发出全部请求,全部成功后按原始顺序拿到 50 个向量,一次性写库;任一请求失败则整体快速失败,触发重试逻辑。

具体案例:知识库导入工具切分 Markdown 文档后,前端用 Promise.all(chunks.map(c => embedApi(c))) 并行向量化 30 个片段,结果按原顺序排列后调用向量数据库批量存入,耗时从串行的 30 秒降到 3 秒。

RAG 检索超时兜底

阶段:检索增强生成 / 服务降级

解决的问题:向量检索服务偶发卡顿,用户提问后白屏数秒。用 Promise.race 把“检索任务”和“2 秒超时任务”赛跑,超时即降级为关键词搜索,保证用户永远在 2 秒内收到响应,而不是无限等待。

具体案例:企业内部问答机器人接入 Promise.race([vectorSearch(q), timeoutReject(2000)]),向量库慢时自动走 Elasticsearch 关键词检索,兜底结果也能满足大部分查询,整体可用性从 95% 提升到 99.5%。

Python 演示

python51 行
import asyncio  async def fetch_user(user_id):    await asyncio.sleep(0.1)    return {"user_id": user_id, "name": "Ada"}  async def check_permission(user):    await asyncio.sleep(0.1)    user["permission"] = "admin"    return user  async def load_dashboard(user):    await asyncio.sleep(0.1)    return {"user": user, "widgets": ["chart", "table"]}  async def fetch_data(name, delay):    await asyncio.sleep(delay)    return name  async def main():    # 链式:上一步结果传给下一步,等价于 Promise 链的 return 传递    user = await fetch_user(1)    user = await check_permission(user)    dashboard = await load_dashboard(user)    print("链式结果:", dashboard)     # 并行:等价于 Promise.all,gather 等待全部完成并保序    parallel = await asyncio.gather(        fetch_data("embedding-a", 0.2),        fetch_data("embedding-b", 0.3),    )    print("并行结果:", parallel)     # 竞速:等价于 Promise.race,FIRST_COMPLETED 取最先 settle 的任务    done, pending = await asyncio.wait(        [fetch_data("fast", 0.1), fetch_data("slow", 0.4)],        return_when=asyncio.FIRST_COMPLETED,    )    for task in done:        print("竞速胜出:", task.result())    for task in pending:        task.cancel()  if __name__ == "__main__":    asyncio.run(main())
目的:用 Python asyncio 复刻 Promise 链式调用的三种核心玩法——串行链、并行聚合、竞速取胜,帮助你在非 JS 环境也能理解异步流水线思维。
  1. user = await check_permission(user)串行链的关键:上一步返回值原样喂给下一步,对应 JS 里 then 的 return 传递。
  2. await asyncio.gather(...)等价于 Promise.all,并发发起两个任务并等待全部完成,结果按传入顺序返回。
  3. return_when=asyncio.FIRST_COMPLETED等价于 Promise.race,只关心最先完成的任务,忽略其余。
  4. task.cancel()竞速分出胜负后清理落败任务,防止后台任务空转。
  5. asyncio.run(main())程序入口,启动整个事件循环,就像前端里那根接通传送带的总电闸。

常见误区

误区我在 then 里执行了异步操作,结果应该会自动传给下一个 then 吧?我写的时候没写 return,发现下一层拿到的怎么是 undefined。

正解Promise 链的传递全靠 return 完成。如果 then 的回调没有返回值,引擎只能把 undefined 作为结果传给下一步——就像工人加工完零件却忘了放上传送带,下游工位只能对空气操作。修正很简单:任何需要在下一步使用的值,都必须显式 return。可以理解为:传送带不读心,只递手上有的东西。

为何常见:许多人把“回调函数中写代码”和“回调函数返回值”混为一谈,以为执行了就有结果,忽略了 return 是函数产出结果的唯一通道。

误区既然拿不到返回值,那我干脆在 then 里再写一个 `.then()` 来处理我新起的 Promise,反正功能是对的。

正解这就是回调地狱复活的根源。正确做法是把新 Promise 直接 return 出去,让外层链“接管”它,等它完成后继续。嵌套写法会让错误处理分叉、缩进爆炸、调试困难。记住那条扁平化规则:你只需要把 Promise 交给传送带,传送带会帮你等它,不用自己手动解包再手动续链。

为何常见:初学者刚学会 Promise 还不熟悉“自动扁平化”机制,本能地沿用了回调里继续回调的旧习惯。

误区catch 不是兜底的吗?那我把它放在链尾就万事大吉了,前面的 then 出错都会掉到那里。

正解catch 出现在不同位置,语义天差地别。链尾 catch 是“整条链的安全网”;中间 catch 则会接住它之前那一段的错误,并且 catch 之后链还能继续走。如果你需要在错误后执行恢复逻辑(比如用缓存兜底再继续渲染),就要把 catch 放在恰当位置。放错位置,轻则接不到预期的错,重则错误被中间的 catch 静默吞掉。

为何常见:把 catch 理解成了“全局异常处理器”,忽略了它本质上只是 then 的第二个参数,作用范围遵循链式结构的位置规则。

误区我用了 Promise.all 让它们“按顺序执行”,把任务一个一个排好队等结果。

正解Promise.all 的核心语义是并发发起,所有任务同时启动,不存在先后等待关系;它只是保证“全部完成后按传入顺序聚合结果”。如果任务间有依赖,all 并不适合,应该用串行链。可以这样记:all 是“多车间同时开工、总装线等待齐套”,不是“一条流水线依次加工”。

为何常见:结果数组的顺序恰好和传入顺序一致,让人误以为执行也按这个顺序,混淆了“启动顺序”和“结果排列顺序”。

面试题

Q1. 在上一个 then 中返回一个普通字符串 "ok",下一个 then 的回调参数是什么?

单选题 · easy

  • A.上一个 Promise 的原始值
  • B.字符串 "ok"← 正确答案
  • C.undefined
  • D.被包装的 Promise 对象

答案:B。then 回调返回普通值时,引擎会把它自动包装成 fulfilled 状态并传给下一个 then,所以下一个 then 拿到的就是 "ok" 本身。就像工人放上传送带的工件,下游原样收到。选项 A 是旧值,C 是忘记 return 的结果,D 是返回 Promise 时的内部处理,用户感知不到包装层。

Q2. 在 then 回调中返回一个 Promise 时,后续 then 会如何表现?

单选题 · easy

  • A.立即执行,拿到的还是旧值
  • B.等待该 Promise 完成后再继续← 正确答案
  • C.直接忽略该 Promise
  • D.自动抛出类型错误

答案:B。返回 Promise 时,外层链会接管它并原地等待,直到它 settle 后把最终结果传下去。这正是“扁平化”机制,无需手动嵌套。A 是未接管的表现,C 和 D 都不符合规范。类比工厂里委托单:传送带会等委托车间完工,再继续运转。

Q3. Promise.reject('err').catch(e => e + '!').then(v => v) 最终 v 的值是?

单选题 · easy

  • A.'err'
  • B.'err!'← 正确答案
  • C.undefined
  • D.抛错

答案:B。catch 本质是 then 的失败回调,它返回 'err!' 后链恢复正常状态,下一个 then 收到该字符串。这体现“catch 不是终点,而是回收站”。A 是错误原始值,C 是 catch 没返回值的假象,D 忽略了 catch 的恢复能力。

Q4. 以下哪种写法会导致下一步 then 收到 undefined?

单选题 · mid

  • A.then(x => x + 1)
  • B.then(x => { x + 1; })← 正确答案
  • C.then(async x => x + 1)
  • D.then(x => Promise.resolve(x + 1))

答案:B。B 的回调体内执行了 x+1 却没有 return,函数返回 undefined,下一步就会拿到 undefined。A 和 C 都有返回值,D 返回 Promise 同样会被正确传递。这就像工人加工了零件却忘了放上传送带,下游只能面对空气。

Q5. 链上第二步 reject 后,若后面没有 catch,最终会怎样?

单选题 · mid

  • A.错误被静默吞掉
  • B.最后一个 then 收到错误
  • C.产生未处理的 promise rejection← 正确答案
  • D.自动恢复成功

答案:C。链上错误会一路冒泡,若没有任何 catch 兜底,就会形成“未处理的 Promise rejection”,在浏览器控制台报错、在 Node 环境可能触发进程告警。A 是错误的乐观猜测,B 的 then 只接成功值,D 违背错误传播机制。好比流水线警报响了却没有回收站,次品只能滚出车间造成隐患。

Q6. 关于 Promise.all,以下哪项描述正确?

单选题 · mid

  • A.任务按数组顺序依次执行
  • B.全部成功时按传入顺序聚合结果← 正确答案
  • C.任一任务失败后其他任务会被取消
  • D.第一个成功的结果就决定最终值

答案:B。Promise.all 并发启动所有任务,全部成功后按传入顺序返回结果数组。它不会取消其他任务,只是整体进入 rejected;也只看首个失败,不看首个成功。A 混淆了“启动”与“排序”,C 是常见误解,D 是 race 的语义。

Q7. 想要实现“请求 3 秒内未完成就当超时处理”,最合适的方式是?

单选题 · hard

  • A.Promise.race([request, timeoutReject(3000)])← 正确答案
  • B.Promise.all([request, timeoutResolve(3000)])
  • C.在 then 里用 setTimeout 判断
  • D.给请求增加无限重试

答案:A。race 让真实请求和 3 秒定时器赛跑,定时器先触发则整体 reject,实现超时降级。B 的 all 会等两个都完成,起不到“提前结束”作用;C 的 setTimeout 只能标记不能终止;D 无限重试只会放大超时问题。这相当于在赛道上放一个“准时炸弹”,先炸先用。

Q8. 关于 then 回调中返回 Promise 的正确说法有?

多选题 · hard

  • A.外层链会等待该 Promise 完成← 正确答案
  • B.与嵌套 then 相比写法更扁平← 正确答案
  • C.该 Promise 的最终值会传给下一步← 正确答案
  • D.会创建新的线程来执行它

答案:ABC。返回 Promise 时外层链原地等待(A 对)、写法扁平化避免回调地狱(B 对)、最终值自动传递(C 对)。Promise 不涉及多线程,JS 依然是单线程事件循环,D 错误。这是“委托单交给传送带”模式的核心,A/B/C 共同构成扁平化机制。

Q9. 哪些写法会导致链式传递失败或取值异常?

多选题 · mid

  • A.then 里执行异步操作但没有 return 它← 正确答案
  • B.then 里同步 throw 一个错误
  • C.then 里调用一个 async 函数但不 return← 正确答案
  • D.catch 里返回一个普通值

答案:AC。A 和 C 本质相同——异步操作没有作为返回值交给链,下一步只能拿到 undefined。B 是正常的错误抛出,会被 catch 捕获,属于受控流程。D 是 catch 的合法恢复方式,链会继续。A/C 才是“工人加工完忘放上传送带”的事故。

Q10. 关于 Promise.all 失败行为的描述,正确的是?

多选题 · extreme

  • A.首个失败的 reject 值会成为整体错误← 正确答案
  • B.失败后依然会等待其他任务完成再报错
  • C.其他任务不会被取消,仍会继续执行← 正确答案
  • D.失败后聚合结果数组会被保留

答案:AC。Promise.all 采取“快速失败”,首个失败立即 reject(A 对);但它不会取消其他任务,那些 Promise 照样在后台执行(C 对)。B 是“全部完成后才报错”的错误理解,D 中聚合数组只在全部成功时产生。这像总装线看到一箱零件有次品立即叫停,但其他车间不知情仍继续生产。