面试高频:手把手教你实现 Promise.all
✍️ 面试高频手把手教你实现Promise.allPromise.all的核心行为是什么在写代码之前我们必须明确Promise.all的契约Contract输入接收一个可迭代对象通常是数组成员可以是 Promise 实例也可以是普通值。输出返回一个新的 Promise。成功条件只有当所有输入的 Promise 都变为fulfilled时返回的 Promise 才变为fulfilled。结果顺序返回的结果数组必须与输入数组的顺序一致而不是完成的先后顺序。失败条件只要有一个输入的 Promise 变为rejected返回的 Promise 立即变为rejected并携带第一个失败的原因快速失败。空数组处理如果输入是空数组直接返回一个已 resolved 的空数组[]。通俗比喻Promise.all像是一个班主任带着全班同学一组 Promise去体检。只有当最后一个同学体检完且合格班主任才会说“全班通过”返回结果数组。如果任何一个同学体检不合格班主任立刻大喊“全班不通过”并记下是谁出了问题抛出错误不再等待其他人。无论谁先体检完最后交上来的成绩单顺序必须按学号输入顺序排列不能乱。 目录️ 实现思路拆解 逐步代码实现⚠️ 关键细节为什么需要计数器 测试与验证 总结1. ️ 实现思路拆解要实现Promise.all我们需要解决三个核心问题如何并行执行遍历输入数组立即启动所有的 Promise或将其包装为 Promise。如何收集结果并保持顺序创建一个与输入等长的结果数组results。每个 Promise 完成后将结果填入对应的索引位置results[index] value。如何知道所有任务都完成了使用一个计数器count。每完成一个任务count。当count 输入数组长度时调用resolve(results)。2. 逐步代码实现第一步基础骨架functionmyPromiseAll(promises){returnnewPromise((resolve,reject){// 1. 校验输入是否为可迭代对象简化版只处理数组if(!Array.isArray(promises)){returnreject(newTypeError(Argument is not iterable));}// 2. 处理空数组情况if(promises.length0){returnresolve([]);}// 3. 准备容器constresultsnewArray(promises.length);// 保持长度初始为 emptyletcompletedCount0;// 计数器// 4. 遍历执行promises.forEach((item,index){// ... 核心逻辑在这里});});}第二步处理每个元素核心逻辑我们需要处理两种情况item本身不是 Promise如数字、字符串直接视为成功。item是 Promise监听其状态。为了统一处理我们可以使用Promise.resolve(item)。它会将普通值包裹成 resolved 的 Promise如果是 Promise 则原样返回。promises.forEach((item,index){// 统一包装确保它是 PromisePromise.resolve(item).then((value){// ✅ 成功逻辑results[index]value;// 1. 存入对应位置completedCount;// 2. 计数加 1// 3. 检查是否全部完成if(completedCountpromises.length){resolve(results);}}).catch((reason){// ❌ 失败逻辑快速失败reject(reason);});});第三步完整代码整合/** * 手写实现 Promise.all * param {Array} promises - Promise 数组或包含普通值的数组 * returns {Promise} */functionmyPromiseAll(promises){returnnewPromise((resolve,reject){// 1. 非数组校验if(!Array.isArray(promises)){returnreject(newTypeError(The argument${promises}is not an array));}// 2. 空数组直接返回if(promises.length0){returnresolve([]);}constresults[];letcompletedCount0;// 3. 遍历执行for(leti0;ipromises.length;i){// 使用 Promise.resolve 兼容普通值Promise.resolve(promises[i]).then((value){results[i]value;// 保持顺序的关键直接赋值给索引 icompletedCount;// 当所有任务都完成时resolve 整个结果数组if(completedCountpromises.length){resolve(results);}}).catch((err){// 只要有一个失败立即 rejectreject(err);});}});}3. ⚠️ 关键细节为什么需要计数器很多初学者会问“为什么不直接用results.length来判断”// ❌ 错误做法results.push(value);if(results.lengthpromises.length){...}原因push是按完成顺序添加的而Promise.all要求结果按输入顺序排列。假设 P1 耗时 3秒P2 耗时 1秒。P2 先完成如果用pushresults[0]会是 P2 的结果。但正确的结果应该是results[0]是 P1results[1]是 P2。正确做法预先创建一个固定长度的数组或使用new Array(len)然后通过索引赋值results[i] value。这样无论谁先完成都会乖乖待在属于自己的位置上。4. 测试与验证让我们用几个场景来验证我们的实现。场景一正常成功constp1newPromise((resolve)setTimeout(()resolve(1),1000));constp2newPromise((resolve)setTimeout(()resolve(2),2000));constp33;// 普通值myPromiseAll([p1,p2,p3]).then((res){console.log(res);// 输出: [1, 2, 3]// 注意虽然 p3 最快p1 次之但顺序严格遵循输入数组});场景二快速失败constp1newPromise((_,reject)setTimeout(()reject(Error 1),1000),);constp2newPromise((resolve)setTimeout(()resolve(2),2000));myPromiseAll([p1,p2]).then((res)console.log(res)).catch((err){console.error(err);// 输出: Error 1// p2 还在运行但 myPromiseAll 已经结束了});场景三空数组myPromiseAll([]).then((res){console.log(res);// []});5. 总结关键点实现方式兼容性使用Promise.resolve()包装每个元素兼容普通值顺序保证使用results[index] value而非push完成判断使用completedCount计数器避免依赖数组长度快速失败在.catch中直接调用reject()边界处理检查输入是否为数组处理空数组情况 博主寄语手写Promise.all不仅是为了应付面试更是为了理解并发控制和状态管理的本质。当你理解了如何通过计数器和索引来协调多个异步任务你就掌握了处理复杂异步流的一把钥匙。记住口诀All 方法返新包遍历输入逐个跑。Resolve 包装保兼容索引赋值序不乱。计数累加判结束一人出错全完蛋。希望这篇文档能帮你彻底掌握Promise.all的实现原理如果有疑问欢迎在评论区留言。喜欢这篇文章吗记得点赞、收藏、转发哦❤️