logo

每日前端手写题Day12:手写Promise.all与Promise.race实现解析

作者:demo2025.09.19 12:47浏览量:0

简介:本文深入解析Promise.all与Promise.race的核心机制,通过手写实现帮助开发者掌握异步编程关键技巧,提升代码质量与调试能力。

一、为何需要手写Promise工具方法?

Promise作为ES6引入的异步处理方案,极大简化了回调地狱问题。但实际开发中,我们常依赖Promise.allPromise.race等工具方法处理并发请求。手写这些方法不仅能加深对Promise工作原理的理解,还能在特殊场景下(如需要定制错误处理逻辑)实现更灵活的控制。

典型应用场景

  1. 批量请求处理:同时发起多个API请求,等待全部完成或任一完成
  2. 超时控制:设置请求超时机制,避免长时间等待
  3. 优先级请求:按优先级顺序处理多个异步任务

二、Promise.all实现详解

核心机制

Promise.all接收一个Promise对象数组,返回一个新的Promise:

  • 当所有输入Promise都成功时,返回包含各结果的数组(顺序与输入一致)
  • 当任一Promise失败时,立即返回第一个失败的Promise结果

手写实现代码

  1. function myPromiseAll(promises) {
  2. return new Promise((resolve, reject) => {
  3. // 处理空数组情况
  4. if (promises.length === 0) {
  5. resolve([]);
  6. return;
  7. }
  8. const results = [];
  9. let completedCount = 0;
  10. promises.forEach((promise, index) => {
  11. // 确保每个元素都是Promise
  12. Promise.resolve(promise)
  13. .then(value => {
  14. results[index] = value;
  15. completedCount++;
  16. if (completedCount === promises.length) {
  17. resolve(results);
  18. }
  19. })
  20. .catch(error => {
  21. reject(error);
  22. });
  23. });
  24. });
  25. }

关键点解析

  1. 参数验证:使用Promise.resolve()确保非Promise值也能被处理
  2. 顺序保持:通过索引index保持结果数组顺序与输入一致
  3. 完成判断:使用计数器completedCount精确判断所有Promise完成时机
  4. 错误处理:任一Promise失败立即触发reject

测试用例

  1. const p1 = Promise.resolve(1);
  2. const p2 = new Promise(resolve => setTimeout(() => resolve(2), 100));
  3. const p3 = Promise.reject('error');
  4. myPromiseAll([p1, p2, p3])
  5. .then(console.log) // 不会执行
  6. .catch(console.error); // 输出: "error"

三、Promise.race实现详解

核心机制

Promise.race接收一个Promise对象数组,返回一个新的Promise:

  • 当任一输入Promise解决或拒绝时,立即返回该结果
  • 后续Promise的结果将被忽略

手写实现代码

  1. function myPromiseRace(promises) {
  2. return new Promise((resolve, reject) => {
  3. if (promises.length === 0) {
  4. // 空数组处理:根据ES规范,应该永远挂起
  5. // 这里简化处理为立即resolve,实际应根据需求调整
  6. resolve(undefined);
  7. return;
  8. }
  9. promises.forEach(promise => {
  10. Promise.resolve(promise)
  11. .then(resolve)
  12. .catch(reject);
  13. });
  14. });
  15. }

关键点解析

  1. 竞态条件处理:第一个完成(无论成功失败)的Promise决定结果
  2. 资源释放:后续Promise的结果被忽略,但仍在执行(需注意资源泄漏)
  3. 空数组处理:规范未明确,实际开发中应根据业务需求决定

实际应用示例:请求超时控制

  1. function fetchWithTimeout(url, timeout = 5000) {
  2. const fetchPromise = fetch(url);
  3. const timeoutPromise = new Promise((_, reject) =>
  4. setTimeout(() => reject(new Error('Request timeout')), timeout)
  5. );
  6. return myPromiseRace([fetchPromise, timeoutPromise]);
  7. }
  8. // 使用示例
  9. fetchWithTimeout('https://api.example.com/data')
  10. .then(response => console.log('Success:', response))
  11. .catch(error => console.error('Failed:', error));

四、进阶实现与优化

带进度的Promise.all

  1. function myPromiseAllWithProgress(promises, onProgress) {
  2. return new Promise((resolve, reject) => {
  3. const results = [];
  4. let completedCount = 0;
  5. promises.forEach((promise, index) => {
  6. Promise.resolve(promise)
  7. .then(value => {
  8. results[index] = value;
  9. completedCount++;
  10. onProgress && onProgress(completedCount / promises.length);
  11. if (completedCount === promises.length) {
  12. resolve(results);
  13. }
  14. })
  15. .catch(reject);
  16. });
  17. });
  18. }

错误处理增强版

  1. function myPromiseAllSafe(promises, defaultValues = []) {
  2. return new Promise((resolve) => {
  3. const results = [];
  4. let completedCount = 0;
  5. promises.forEach((promise, index) => {
  6. Promise.resolve(promise)
  7. .then(value => {
  8. results[index] = value;
  9. completedCount++;
  10. if (completedCount === promises.length) {
  11. resolve(results);
  12. }
  13. })
  14. .catch(() => {
  15. results[index] = defaultValues[index] || null;
  16. completedCount++;
  17. if (completedCount === promises.length) {
  18. resolve(results);
  19. }
  20. });
  21. });
  22. });
  23. }

五、最佳实践建议

  1. 输入验证:在实际项目中应添加参数类型检查
  2. 性能优化:对于大量Promise,考虑分批处理
  3. 错误处理:根据业务需求决定是立即失败还是收集所有错误
  4. 取消机制:可结合AbortController实现请求取消
  5. TypeScript支持:添加类型定义提升代码可靠性

六、常见问题解答

Q1: 为什么我的实现中结果顺序不对?
A1: 必须使用输入时的索引来存储结果,不能简单push到数组

Q2: 如何处理非Promise输入?
A2: 使用Promise.resolve()包装每个输入项,确保统一处理

Q3: 空数组应该返回什么?
A3: 根据ES规范,Promise.all([])应返回已解决的Promise(值为空数组),Promise.race([])应永远挂起

通过系统掌握这些核心方法的实现原理,开发者不仅能更灵活地处理异步场景,还能在面试和实际项目中展现出更深的技术功底。建议结合实际项目需求,对这些基础实现进行扩展和优化。

相关文章推荐

发表评论