前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库reduce 串行 Promise
JavaScript异步与 Promise

一组返回 Promise 的函数,怎么用 reduce 串行执行并把上一个结果传给下一个?

以 Promise.resolve(initial) 作为初始值,每一步写 prev.then(fn),这样 fn 会在上一个 Promise 兑现后才被调用,参数就是上一步的结果。关键是传给 then 的必须是函数本身,写成 fn() 就变成全部同时执行了。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#异步与 Promise#异步编程
先看核心答案读代码示例
理解线索

串行执行靠的三样东西

  1. reduce 的累加器一直是「到目前为止的最后一个 Promise」
  2. then(fn)等累加器兑现再调用 fn,并把兑现值传进去
  3. 返回的新 Promisefn 返回的 Promise 被 then 展开,成为下一步的累加器

reduce 本身是同步跑完的,它只是一口气把整条 then 链搭好了。真正按顺序执行的是这条链,不是 reduce 的遍历过程。

核心回答

先记住这个答案

写成 fns.reduce((prev, fn) => prev.then(fn), Promise.resolve(initial))。累加器 prev 始终是上一步返回的 Promise,prev.then(fn) 会等它兑现后再调用 fn,并把兑现值作为参数传进去;fn 返回的 Promise 又成为下一步的 prev。这样每个函数都在前一个完成后才开始,而且拿到的是前一个的结果。最常见的错误是把 fn 写成 fn(),那样所有函数会在 reduce 遍历时同时启动,串行就失效了。

  • reduce 的初始值用 Promise.resolve(initial)
  • 每一步 prev.then(fn),上一步的结果自动成为参数
  • 写成 prev.then(fn()) 会让所有任务同时启动
  • 任何一步失败,后面的都跳过,在末尾 catch 一次即可

标准写法和执行过程

下面三个函数,第一个最慢、第二个最快,如果是并发执行,完成顺序会乱。

用 then 把函数一个个接起来JavaScript
const tasks = [
  (x) => new Promise((r) => setTimeout(() => r(x + 1), 30)),
  (x) => new Promise((r) => setTimeout(() => r(x * 2), 10)),
  (x) => Promise.resolve(x - 3)
]

function runSerial(fns, initial) {
  return fns.reduce((prev, fn) => prev.then(fn), Promise.resolve(initial))
}

runSerial(tasks, 5).then((v) => console.log('结果', v)) // (5 + 1) * 2 - 3
查看输出与解释
结果 9

reduce 同步地把链搭成了 Promise.resolve(5).then(t1).then(t2).then(t3)。然后链开始执行:30 毫秒后 t1 得到 6,t2 这时才被调用,再过 10 毫秒得到 12,最后 t3 得到 9。t2 虽然最快,也要等 t1 结束才开始。 初始值一定要是 Promise。如果写成 reduce(fn, 5),第一步的 prev 是数字 5,数字上没有 then 方法,会直接报 TypeError。

用 async/await 写同样的事
fns.reduce(async (prev, fn) => fn(await prev), initial) 效果一样。每个 async 回调会先 await 上一步的 Promise,拿到结果后才调用 fn。不过面试之外,更推荐直接写 for...of 循环加 await,读的人一眼就能看出是串行,调试时也能逐步看到中间值。

经典错误:then 里写成了 fn()

这个错误的迷惑性在于代码能跑、不报错,只是顺序和结果都不对。

看起来是串行,其实是并发JavaScript
const make = (name, ms, log) => () =>
  new Promise((r) => setTimeout(() => { log.push(name); r(name) }, ms))

const log = []
const fns = [make('A', 30, log), make('B', 10, log), make('C', 20, log)]

fns.reduce((prev, fn) => prev.then(fn()), Promise.resolve()) // fn() 立刻执行
  .then(() => console.log('链结束时完成了', log.length, '个'))

setTimeout(() => console.log('实际完成顺序', log.join(',')), 100)
查看输出与解释
链结束时完成了 0 个
实际完成顺序 B,C,A

fn() 在 reduce 遍历时就被调用了,三个定时器同时启动,完成顺序变成了按耗时排的 B、C、A。更糟的是,then 收到的不是函数而是一个 Promise,规范规定非函数参数直接忽略,所以这条链根本没在等任何任务,一开始就结束了。 改法就是把 fn 本身交给 then。如果还需要额外参数,写成 prev.then((result) => fn(result, extra)),同样是传一个函数。

回答前,多想一步

容易答错的地方

以为 reduce 会等每一步完成
reduce 是同步方法,它不认识 Promise,几乎瞬间就遍历完了。它做的只是把 then 链搭起来,等待是由这条链完成的。所以在 reduce 回调里直接调用 fn(),任务就在搭链时启动了。
忘了处理中间某一步失败
链上任何一步失败,后面的 then(fn) 都会被跳过,错误一路传到最后。只要在 runSerial 返回的 Promise 上接一个 catch 就能拿到。如果希望某一步失败了也继续,要在那一步自己 catch 并返回一个默认值。
试着用自己的话回答

面试官还会怎么问?

如果要收集每一步的结果,而不只是最后一个?

让累加器兑现为一个数组:初始值用 Promise.resolve([]),每一步写成 prev.then((list) => fn(list[list.length - 1]).then((v) => [...list, v]))。最后得到的数组就是每一步的结果,顺序和函数数组一致。

和 Promise.all 有什么区别?

Promise.all 接收的是已经启动的 Promise,任务是并发执行的,只是等全部完成。reduce 串行接收的是函数,前一个完成才启动下一个。任务之间有依赖,或者必须控制对服务端的压力时,用串行。

想要既不是全串行也不是全并发,比如同时最多 3 个,怎么写?

维护一个正在执行的任务计数。启动时先开 3 个,每完成一个就从队列里取下一个补上,全部完成后再兑现总的 Promise。这就是常考的并发控制题,本质是把串行的「一个接一个」换成「三条线各自串行」。

从一道题,走向一组知识

把知识连起来

数据处理

ArrayBuffer 和 SharedArrayBuffer 有什么区别,分别用在什么场景?

串行上传大文件时每个分片就是一个 ArrayBuffer,可以顺着了解它和 SharedArrayBuffer 的区别。

数据处理

ArrayBuffer 的 transfer 和 slice 有什么区别,处理大数据时该用哪个?

把分片交给 Worker 处理时,用 transfer 转移 ArrayBuffer 而不是 slice 复制,能省掉一次拷贝。

参考资料

  • JavaScript:异步与 Promise参考资料
  • Array.prototype.reduce()

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 标准写法和执行过程
  3. 经典错误:then 里写成了 fn()
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑