深入理解 Promise 的高阶用法:从入门到手写实现

今天,我想和大家深入聊聊 Promise 的几个高阶方法:Promise.allPromise.racePromise.allSettledPromise.any。这些方法在处理多个异步任务时非常有用,也是面试中的常客。我会从基本用法讲起,结合实际例子,最后手写实现这些方法,并附上常见的面试题。

1. Promise.all:等待所有任务完成

1.1 基本概念

Promise.all 是一个静态方法,它接收一个可迭代对象(通常是数组),其中包含多个 Promise 实例。它的作用是等待所有 Promise 都成功完成 ,然后返回一个新 Promise,该 Promise 的结果是一个包含所有 Promise 结果的数组。

关键点:

  • 所有 Promise 都必须成功,Promise.all 返回的 Promise 才会成功。
  • 如果其中任何一个 Promise 失败(rejected),Promise.all 会立即失败,并返回第一个失败的 Promise 的错误。

1.2 使用场景

想象一下,你正在开发一个电商网站,需要同时获取用户信息、购物车数据和推荐商品列表。这三个请求是独立的,但你希望在所有数据都准备好后,再渲染页面。

javascript 复制代码
// 模拟三个异步请求
function fetchUserInfo() {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ name: '张三', age: 25 }), 1000);
    });
}

function fetchCartData() {
    return new Promise((resolve) => {
        setTimeout(() => resolve(['商品A', '商品B']), 800);
    });
}

function fetchRecommendations() {
    return new Promise((resolve) => {
        setTimeout(() => resolve(['商品C', '商品D']), 1200);
    });
}

// 使用 Promise.all
Promise.all([fetchUserInfo(), fetchCartData(), fetchRecommendations()])
    .then(([userInfo, cartData, recommendations]) => {
        console.log('用户信息:', userInfo);
        console.log('购物车:', cartData);
        console.log('推荐商品:', recommendations);
        // 渲染页面
    })
    .catch(error => {
        console.error('获取数据失败:', error);
        // 处理错误,比如显示错误提示
    });

在这个例子中,Promise.all 等待所有三个请求都成功后,才执行 .then 回调。如果任何一个请求失败(比如网络错误),Promise.all 会立即进入 .catch 分支。

1.3 手写实现

现在我们来手写一个 Promise.all 的简化版本:

javascript 复制代码
function myPromiseAll(promises) {
    // 返回一个新的 Promise
    return new Promise((resolve, reject) => {
        // 如果传入的不是可迭代对象,直接 resolve 空数组
        if (!Array.isArray(promises)) {
            return resolve([]);
        }

        const results = [];
        let completedCount = 0;

        // 如果传入空数组,直接 resolve
        if (promises.length === 0) {
            return resolve(results);
        }

        promises.forEach((promise, index) => {
            // 使用 Promise.resolve 确保每个元素都是 Promise
            Promise.resolve(promise)
                .then(value => {
                    results[index] = value;
                    completedCount++;

                    // 当所有 Promise 都完成时,resolve 结果数组
                    if (completedCount === promises.length) {
                        resolve(results);
                    }
                })
                .catch(error => {
                    // 任何一个 Promise 失败,立即 reject
                    reject(error);
                });
        });
    });
}

// 测试
myPromiseAll([
    Promise.resolve('第一个'),
    new Promise(resolve => setTimeout(() => resolve('第二个'), 500)),
    '第三个' // 普通值也会被包装成 Promise
]).then(results => {
    console.log(results); // ['第一个', '第二个', '第三个']
});

1.4 注意事项

  • Promise.all 的结果数组顺序与传入的 Promise 数组顺序一致,不保证执行顺序
  • 如果传入的 Promise 中有 rejectPromise.all 会立即失败,不会等待其他 Promise 完成。

2. Promise.race:谁先完成就用谁

2.1 基本概念

Promise.race 也是接收一个 Promise 数组,但它只关心第一个完成的 Promise ,无论是成功还是失败。一旦有 Promise 完成,Promise.race 就会立即返回那个结果。

关键点:

  • 返回第一个完成的 Promise 的结果或错误。
  • 适用于超时控制或竞态场景。

2.2 使用场景

最常见的用途是实现请求超时。比如,我们希望一个请求在 3 秒内完成,否则就认为它失败了。

javascript 复制代码
// 模拟一个可能很慢的请求
function slowRequest() {
    return new Promise(resolve => {
        setTimeout(() => resolve('请求成功'), 5000); // 5秒
    });
}

// 创建一个超时的 Promise
function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => reject(new Error(`请求超时: ${ms}ms`)), ms);
    });
}

// 使用 Promise.race 实现超时
Promise.race([slowRequest(), timeout(3000)])
    .then(result => {
        console.log('成功:', result);
    })
    .catch(error => {
        console.error('失败:', error.message); // 输出: 失败: 请求超时: 3000ms
    });

在这个例子中,timeout(3000) 会在 3 秒后 reject,而 slowRequest() 需要 5 秒。由于 timeout 先完成,Promise.race 会立即进入 .catch 分支。

2.3 手写实现

javascript 复制代码
function myPromiseRace(promises) {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promises) || promises.length === 0) {
            return reject(new Error('Promise.race requires a non-empty iterable'));
        }

        promises.forEach(promise => {
            Promise.resolve(promise)
                .then(resolve)  // 第一个 resolve 或 reject 会决定最终结果
                .catch(reject);
        });
    });
}

// 测试
myPromiseRace([
    new Promise(resolve => setTimeout(() => resolve('A'), 100)),
    new Promise(resolve => setTimeout(() => resolve('B'), 50)),
    new Promise((_, reject) => setTimeout(() => reject(new Error('C')), 30))
])
.then(result => {
    console.log(result); // 不会执行,因为 C 先 reject
})
.catch(error => {
    console.error(error.message); // 输出: C
});

2.4 注意事项

  • Promise.race 的"完成"包括 resolvereject。只要有一个 Promise 结束,它就结束。
  • 适合用于竞态场景,比如多个数据源中取最快的一个。

3. Promise.allSettled:等待所有任务尘埃落定

3.1 基本概念

Promise.allSettled 是 ES2020 引入的新方法。它和 Promise.all 类似,都会等待所有 Promise 完成,但关键区别在于:它不会因为某个 Promise 失败而中断 。无论成功还是失败,它都会等待所有 Promise 结束,并返回一个描述每个 Promise 结果的对象数组。

返回值结构:

  • 成功的 Promise{ status: 'fulfilled', value: result }
  • 失败的 Promise{ status: 'rejected', reason: error }

3.2 使用场景

当你需要知道所有 异步任务的结果,而不关心它们是否成功时,Promise.allSettled 就派上用场了。比如批量上传文件,你希望知道每个文件的上传结果,而不是因为一个失败就放弃。

javascript 复制代码
function uploadFile(filename) {
    return new Promise((resolve, reject) => {
        const success = Math.random() > 0.3; // 模拟 70% 成功率
        setTimeout(() => {
            success ? resolve(`${filename} 上传成功`) : reject(new Error(`${filename} 上传失败`));
        }, 1000);
    });
}

const files = ['file1.txt', 'file2.txt', 'file3.txt', 'file4.txt'];

Promise.allSettled(files.map(uploadFile))
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`${files[index]}: ✅ ${result.value}`);
            } else {
                console.log(`${files[index]}: ❌ ${result.reason.message}`);
            }
        });
    });

输出可能类似于:

makefile 复制代码
file1.txt: ✅ file1.txt 上传成功
file2.txt: ❌ file2.txt 上传失败
file3.txt: ✅ file3.txt 上传成功
file4.txt: ✅ file4.txt 上传成功

3.3 手写实现

javascript 复制代码
function myPromiseAllSettled(promises) {
    return new Promise((resolve) => {
        if (!Array.isArray(promises)) {
            return resolve([]);
        }

        const results = [];
        let settledCount = 0;

        if (promises.length === 0) {
            return resolve(results);
        }

        promises.forEach((promise, index) => {
            Promise.resolve(promise)
                .then(value => {
                    results[index] = { status: 'fulfilled', value };
                    settledCount++;
                    if (settledCount === promises.length) {
                        resolve(results);
                    }
                })
                .catch(reason => {
                    results[index] = { status: 'rejected', reason };
                    settledCount++;
                    if (settledCount === promises.length) {
                        resolve(results);
                    }
                });
        });
    });
}

// 测试
myPromiseAllSettled([
    Promise.resolve('成功1'),
    Promise.reject(new Error('失败1')),
    '普通值'
]).then(results => {
    console.log(results);
    // [
    //   { status: 'fulfilled', value: '成功1' },
    //   { status: 'rejected', reason: Error: 失败1 },
    //   { status: 'fulfilled', value: '普通值' }
    // ]
});

3.4 注意事项

  • Promise.allSettled 永远不会 reject,它总是 resolve 一个结果数组。
  • 适合用于批量操作,需要收集所有结果的场景。

4. Promise.any:至少有一个成功

4.1 基本概念

Promise.any 是 ES2021 引入的方法。它会等待第一个成功(fulfilled)的 Promise ,并返回其结果。只有当所有 Promise 都失败时,它才会 reject,并返回一个 AggregateError,其中包含所有失败的原因。

关键点:

  • 返回第一个成功的 Promise 的结果。
  • 只有当所有 Promise 都失败时,才会 reject。

4.2 使用场景

想象你需要从多个镜像源下载一个文件,只要有一个源成功,就可以下载。这比 Promise.race 更合适,因为 race 会返回第一个结果(可能是失败),而 any 只关心成功。

javascript 复制代码
function downloadFromSource(source) {
    return new Promise((resolve, reject) => {
        const success = Math.random() > 0.6; // 模拟 40% 成功率
        setTimeout(() => {
            success ? resolve(`从 ${source} 下载成功`) : reject(new Error(`从 ${source} 下载失败`));
        }, 1000 + Math.random() * 1000);
    });
}

const sources = ['源A', '源B', '源C', '源D'];

Promise.any(sources.map(downloadFromSource))
    .then(result => {
        console.log('下载成功:', result);
    })
    .catch(error => {
        console.error('所有源都失败:', error.errors); // AggregateError
    });

如果至少有一个源成功,就会进入 .then;如果全部失败,才会进入 .catch

4.3 手写实现

javascript 复制代码
function myPromiseAny(promises) {
    return new Promise((resolve, reject) => {
        if (!Array.isArray(promises) || promises.length === 0) {
            return reject(new AggregateError([], 'All promises were rejected'));
        }

        const errors = [];
        let rejectedCount = 0;

        promises.forEach(promise => {
            Promise.resolve(promise)
                .then(resolve) // 第一个成功就 resolve
                .catch(error => {
                    errors.push(error);
                    rejectedCount++;
                    if (rejectedCount === promises.length) {
                        reject(new AggregateError(errors, 'All promises were rejected'));
                    }
                });
        });
    });
}

// 测试
myPromiseAny([
    Promise.reject(new Error('失败1')),
    new Promise(resolve => setTimeout(() => resolve('成功2'), 500)),
    Promise.reject(new Error('失败3'))
])
.then(result => {
    console.log(result); // 成功2
})
.catch(error => {
    console.error('全部失败:', error.errors);
});

4.4 注意事项

  • Promise.any 会忽略失败,直到找到第一个成功。
  • 如果所有 Promise 都失败,它会 reject 一个 AggregateError

5. 四种方法对比总结

方法 成功条件 失败条件 返回值 适用场景
Promise.all 所有成功 任一失败 成功值数组 所有任务必须成功
Promise.race 第一个完成(无论成功/失败) 第一个完成(无论成功/失败) 第一个结果 竞态、超时
Promise.allSettled 所有完成(无论成功/失败) 永不失败 结果对象数组 收集所有结果
Promise.any 第一个成功 所有失败 第一个成功值 至少一个成功

6. 常见面试题

  1. Promise.allPromise.race 的区别是什么?

    • all 等待所有成功,race 等待第一个完成。
  2. 如果 Promise.all 中有一个 Promise 失败,会发生什么?

    • Promise.all 会立即 reject,并返回第一个失败的 Promise 的错误。
  3. Promise.allSettledPromise.all 有什么不同?

    • allSettled 不会因为某个 Promise 失败而中断,它会等待所有 Promise 完成并返回结果。
  4. Promise.any 在什么情况下会 reject?

    • 只有当所有 Promise 都 reject 时,Promise.any 才会 reject。
  5. 手写 Promise.all 的实现。

    • 如上文所示,注意处理空数组、非数组输入和 Promise.resolve 包装。
  6. 如何实现一个带超时功能的 Promise

    • 使用 Promise.race,结合一个定时 reject 的 Promise
  7. Promise.all 的结果数组顺序是否与传入数组一致?

    • 是的,顺序与传入的 Promise 数组顺序一致。
  8. 为什么 Promise.all 要用 Promise.resolve 包装每个元素?

    • 为了确保每个元素都是 Promise,兼容普通值或 thenable 对象。

结语

Promise 的这些高阶方法极大地丰富了我们处理异步操作的能力。理解它们的差异和适用场景,不仅能写出更健壮的代码,也能在面试中游刃有余。记住,选择哪个方法取决于你的业务需求:

  • 全部成功 ?用 all
  • 第一个结果 ?用 race
  • 所有结果 ?用 allSettled
  • 至少一个成功 ?用 any

希望这篇博客能帮你彻底掌握这些 Promise 的高阶用法。在实际开发中多尝试使用它们,你会发现异步编程可以如此优雅。

相关推荐
掘金安东尼12 分钟前
使用自定义高亮API增强用户‘/’体验
前端·javascript·github
参宿71 小时前
electron之win/mac通知免打扰
java·前端·electron
石小石Orz1 小时前
性能提升60%:前端性能优化终极指南
前端·性能优化
夏日不想说话1 小时前
API请求乱序?深入解析 JS 竞态问题
前端·javascript·面试
zhaoolee1 小时前
通过rss订阅小红书,程序员将小红书同步到自己的github主页
前端
掘金安东尼1 小时前
我们让 JSON.stringify 的速度提升了两倍以上
前端·javascript·面试
Cheney95012 小时前
TypeScript 中,! 是 非空断言操作符
前端·vue.js·typescript
sp422 小时前
老旧前端项目如何升级工程化的项目
前端
青山Coding2 小时前
Cesium应用(二):基于heatmap.js 的全球气象可视化实现方案
前端·gis·cesium
羊锦磊2 小时前
[ CSS 前端 ] 网页内容的修饰
java·前端·css