Promise.all 与 Promise.race 的手写实现:如何处理并发请求与超时控制

各位编程爱好者,大家好!

今天,我们将深入探讨 JavaScript 异步编程的核心机制,特别是如何高效地处理并发请求与实现精妙的超时控制。在现代 Web 应用中,异步操作无处不在,从数据抓取到用户界面的响应,它们构成了用户体验的基石。Promises 作为 JavaScript 异步编程的基石,为我们提供了一种结构化、可管理的方式来处理这些操作。

然而,单个 Promise 往往不足以应对复杂的场景。想象一下,你需要同时从多个 API 端点获取数据,或者你需要从多个镜像服务器中选择响应最快的一个,再或者你需要确保某个操作在限定时间内完成。这时,Promise.allPromise.race 就闪亮登场了。它们是 Promise API 提供的两个强大工具,用于协调多个 Promise 的行为。

本次讲座的目标是:

  1. 深入理解 Promise.allPromise.race 的工作原理和常见用例。
  2. 亲手实现 它们的核心逻辑,从而加深对 Promise 内部机制的理解。
  3. 拓展应用 学习如何利用这些基础构建更高级的并发控制(如限制并发数)和超时控制机制。

让我们从 Promises 的基础回顾开始,为后续的深入讨论打下坚实的基础。


理解 JavaScript Promises:异步的契约

在深入 Promise.allPromise.race 之前,我们先快速回顾一下 Promises 的基本概念。

Promise 是一个代表了异步操作最终完成(或失败)的对象。它有三种状态:

  • Pending (待定):初始状态,既没有成功,也没有失败。
  • Fulfilled (已成功):异步操作成功完成,并返回了一个值。
  • Rejected (已失败):异步操作失败,并返回了一个错误原因。

一旦 Promise 从 Pending 状态变为 Fulfilled 或 Rejected,它就进入了 Settled (已解决) 状态,并且状态不可逆转。

我们可以通过 new Promise() 构造函数来创建一个 Promise:

const myAsyncOperation = new Promise((resolve, reject) => {
    // 模拟一个异步操作,例如网络请求或定时器
    const success = Math.random() > 0.5; // 随机决定成功或失败
    setTimeout(() => {
        if (success) {
            resolve("数据已成功获取!"); // 异步操作成功时调用 resolve
        } else {
            reject(new Error("数据获取失败!")); // 异步操作失败时调用 reject
        }
    }, 1000);
});

// 使用 .then() 和 .catch() 来处理 Promise 的结果
myAsyncOperation
    .then(data => {
        console.log("成功:", data);
    })
    .catch(error => {
        console.error("失败:", error.message);
    })
    .finally(() => {
        console.log("操作完成,无论成功或失败。");
    });

.then() 方法用于处理 Promise 成功时的回调,.catch() 用于处理 Promise 失败时的回调,而 .finally() 则无论成功或失败都会执行。


Promise.all:等待所有任务完成的编排者

Promise.all 是一个非常实用的工具,它允许我们并行地执行多个 Promise,并在所有 Promise 都成功完成时,统一处理它们的结果。

Promise.all 的核心行为

  1. 输入: 接受一个可迭代对象(通常是一个数组),其中包含多个 Promise 实例。
  2. 输出: 返回一个新的 Promise 实例。
  3. 成功条件: 只有当输入数组中的所有 Promise 都成功(fulfilled)时,Promise.all 返回的 Promise 才会成功。
    • 成功时,它会返回一个数组,该数组包含了所有输入 Promise 的成功值,并且这些值的顺序与输入 Promise 的顺序严格一致。
  4. 失败条件: 只要输入数组中有一个 Promise 失败(rejected),Promise.all 返回的 Promise 就会立即失败。
    • 失败时,它会返回第一个失败 Promise 的错误信息。
  5. 非 Promise 值: 如果输入数组中包含非 Promise 的值(例如普通数字、字符串),Promise.all 会将其视为一个已经成功(fulfilled)的 Promise,并直接使用该值。
  6. 空数组: 如果输入数组为空,Promise.all 会立即成功,并返回一个空数组。

实际应用场景

  • 并行加载多个资源: 例如,在一个页面中同时加载用户数据、订单列表和配置信息。
  • 等待多个异步计算完成: 当一个操作依赖于多个独立异步计算的结果时。
  • 表单提交前的多项验证: 多个异步验证规则全部通过后才允许提交。

Promise.all 内置用法示例

console.log("--- Promise.all 示例 ---");

const fetchUserData = new Promise(resolve => {
    setTimeout(() => resolve({ id: 1, name: "Alice" }), 1000);
});

const fetchPosts = new Promise(resolve => {
    setTimeout(() => resolve([{ postId: 101, title: "Post 1" }, { postId: 102, title: "Post 2" }]), 1500);
});

const fetchSettings = Promise.resolve({ theme: "dark", notifications: true }); // 立即解决的 Promise

// 场景1: 所有 Promise 都成功
Promise.all([fetchUserData, fetchPosts, fetchSettings])
    .then(results => {
        console.log("所有数据加载成功 (场景1):", results);
        const [userData, posts, settings] = results;
        console.log("用户数据:", userData);
        console.log("帖子:", posts);
        console.log("设置:", settings);
    })
    .catch(error => {
        console.error("数据加载失败 (场景1):", error);
    });

// 场景2: 其中一个 Promise 失败
const fetchErrorData = new Promise((_, reject) => {
    setTimeout(() => reject(new Error("网络请求失败!")), 500); // 提前失败
});

Promise.all([fetchUserData, fetchErrorData, fetchSettings])
    .then(results => {
        console.log("所有数据加载成功 (场景2 - 不会执行):", results);
    })
    .catch(error => {
        console.error("数据加载失败 (场景2):", error.message); // 输出: 网络请求失败!
    });

// 场景3: 空数组
Promise.all([])
    .then(results => {
        console.log("空数组 Promise.all 成功 (场景3):", results); // 输出: []
    });

// 场景4: 包含非 Promise 值
Promise.all([Promise.resolve("Hello"), 123, "World", fetchPosts])
    .then(results => {
        console.log("包含非 Promise 值 (场景4):", results); // 输出: ["Hello", 123, "World", [{...}, {...}]]
    });

手写 myPromiseAll 实现

现在,让我们尝试自己实现一个 Promise.all。通过亲手实现,我们可以更深入地理解其内部逻辑,尤其是如何处理多个异步操作的状态管理和结果收集。

/**
 * 手写实现 Promise.all
 * @param {Iterable<Promise|any>} promises - 一个可迭代对象,通常是 Promise 实例的数组
 * @returns {Promise<Array<any>>} - 返回一个新的 Promise
 */
function myPromiseAll(promises) {
    // 返回一个新的 Promise,它的状态将由输入 Promises 的状态决定
    return new Promise((resolve, reject) => {
        // 将输入的可迭代对象转换为数组,以便于处理
        const promiseArray = Array.from(promises);
        const totalPromises = promiseArray.length;

        // 如果输入数组为空,则立即解决并返回一个空数组
        if (totalPromises === 0) {
            resolve([]);
            return;
        }

        const results = []; // 用于存储每个 Promise 的成功值
        let completedCount = 0; // 记录已完成(成功或失败)的 Promise 数量

        // 遍历输入数组中的每个 Promise
        promiseArray.forEach((promise, index) => {
            // 将当前项包装成 Promise,以确保我们总是在处理 Promise 实例
            // Promise.resolve() 可以将普通值转换为一个已解决的 Promise
            // 如果已经是 Promise,则直接返回自身
            Promise.resolve(promise)
                .then(value => {
                    // 当一个 Promise 成功时,将其值存储在结果数组的正确位置
                    // 保持顺序是 Promise.all 的一个关键特性
                    results[index] = value;
                    completedCount++;

                    // 如果所有 Promise 都已成功完成,则解决 myPromiseAll 返回的 Promise
                    if (completedCount === totalPromises) {
                        resolve(results);
                    }
                })
                .catch(error => {
                    // 只要有一个 Promise 失败,myPromiseAll 返回的 Promise 就会立即失败
                    // 并将第一个失败 Promise 的错误作为其拒绝值
                    reject(error);
                });
        });
    });
}

// 测试 myPromiseAll
console.log("n--- myPromiseAll 示例 ---");

const task1 = new Promise(resolve => setTimeout(() => resolve("Task 1 Done"), 100));
const task2 = new Promise(resolve => setTimeout(() => resolve("Task 2 Done"), 200));
const task3 = "Just a string"; // 非 Promise 值

myPromiseAll([task1, task2, task3])
    .then(values => {
        console.log("myPromiseAll 成功:", values); // ["Task 1 Done", "Task 2 Done", "Just a string"]
    })
    .catch(error => {
        console.error("myPromiseAll 失败:", error);
    });

const failingTask = new Promise((_, reject) => setTimeout(() => reject(new Error("Task Failed!")), 50));

myPromiseAll([task1, failingTask, task2])
    .then(values => {
        console.log("myPromiseAll 成功 (不应执行):", values);
    })
    .catch(error => {
        console.error("myPromiseAll 失败:", error.message); // Task Failed!
    });

myPromiseAll([])
    .then(values => {
        console.log("myPromiseAll 空数组:", values); // []
    });

myPromiseAll 实现思路详解:

  1. 返回新 Promise: myPromiseAll 必须返回一个新的 Promise,因为它的状态和结果需要等待所有输入 Promise 的状态才能确定。
  2. 处理空数组: 这是 Promise.all 的一个特殊规则。如果输入是一个空数组,它应该立即成功并返回一个空数组。
  3. 结果存储: 我们需要一个 results 数组来按顺序存储每个成功 Promise 的值。重要的是要使用 index 来确保结果的顺序与输入 Promise 的顺序一致。
  4. 计数器: completedCount 变量用于跟踪已经完成(无论是成功还是失败)的 Promise 数量。
  5. 迭代并监听:
    • 遍历 promiseArray 中的每一个元素。
    • 使用 Promise.resolve(promise) 将每个元素都确保为一个 Promise。这能处理输入数组中包含非 Promise 值的情况。
    • 对每个处理后的 Promise 附加 .then().catch() 回调。
    • .then() 中,将成功值存入 results[index],并递增 completedCount。如果 completedCount 达到了 totalPromises,说明所有 Promise 都已成功,此时就可以调用外部 Promise 的 resolve(results)
    • .catch() 中,一旦有任何一个 Promise 失败,就立即调用外部 Promise 的 reject(error)。这是 Promise.all 的“全有或全无”特性。

Promise.race:争夺最快完成的竞速者

Promise.race 顾名思义,它让多个 Promise 进行一场“赛跑”,谁先完成(无论是成功还是失败),Promise.race 返回的 Promise 就会采纳谁的结果。

Promise.race 的核心行为

  1. 输入: 接受一个可迭代对象(通常是一个数组),其中包含多个 Promise 实例。
  2. 输出: 返回一个新的 Promise 实例。
  3. 结算条件: 只要输入数组中的任何一个 Promise 最先解决(settled,即变为 fulfilled 或 rejected),Promise.race 返回的 Promise 就会立即以该 Promise 的状态和值/错误为准进行结算。
  4. 非 Promise 值: 如果输入数组中包含非 Promise 的值,它会被视为一个立即解决的 Promise,并且很可能成为“最快”解决的 Promise。
  5. 空数组: 如果输入数组为空,Promise.race 返回的 Promise 将永远处于 Pending 状态,即永远不会解决。

实际应用场景

  • 超时控制: 将一个实际任务 Promise 与一个定时器 Promise 结合,如果定时器先触发,则任务超时。
  • 从多个数据源获取最快响应: 同时向多个服务器发出请求,取第一个返回的结果。
  • 用户交互的竞态条件: 例如,用户点击了一个按钮,在请求发出后又迅速点击了取消按钮,取最先发生的事件。

Promise.race 内置用法示例

console.log("n--- Promise.race 示例 ---");

const slowTask = new Promise(resolve => setTimeout(() => resolve("Slow Task Done"), 500));
const mediumTask = new Promise(resolve => setTimeout(() => resolve("Medium Task Done"), 200));
const fastTask = new Promise(resolve => setTimeout(() => resolve("Fast Task Done"), 100));
const immediateTask = Promise.resolve("Immediate Task Done"); // 立即解决

// 场景1: 最快的 Promise 成功
Promise.race([slowTask, mediumTask, fastTask])
    .then(value => {
        console.log("Promise.race 成功 (场景1):", value); // 输出: Fast Task Done
    })
    .catch(error => {
        console.error("Promise.race 失败 (场景1):", error);
    });

// 场景2: 最快的 Promise 失败
const failingFastTask = new Promise((_, reject) => setTimeout(() => reject(new Error("Fast Failure!")), 50));

Promise.race([slowTask, failingFastTask, mediumTask])
    .then(value => {
        console.log("Promise.race 成功 (场景2 - 不会执行):", value);
    })
    .catch(error => {
        console.error("Promise.race 失败 (场景2):", error.message); // 输出: Fast Failure!
    });

// 场景3: 包含立即解决的非 Promise 值
Promise.race([slowTask, immediateTask, fastTask])
    .then(value => {
        console.log("Promise.race 成功 (场景3):", value); // 输出: Immediate Task Done
    })
    .catch(error => {
        console.error("Promise.race 失败 (场景3):", error);
    });

// 场景4: 空数组
console.log("Promise.race 空数组 (场景4): 返回的 Promise 将永不解决。");
const neverResolves = Promise.race([]);
neverResolves.then(val => console.log("Never resolves with value:", val));
neverResolves.catch(err => console.error("Never resolves with error:", err));
// 实际上,这里不会有任何输出,因为 Promise 永远处于 pending 状态

手写 myPromiseRace 实现

Promise.race 的实现相对 Promise.all 来说更简洁,因为它不需要跟踪所有 Promise 的状态或收集所有结果,只需要关注第一个解决的 Promise。

/**
 * 手写实现 Promise.race
 * @param {Iterable<Promise|any>} promises - 一个可迭代对象,通常是 Promise 实例的数组
 * @returns {Promise<any>} - 返回一个新的 Promise
 */
function myPromiseRace(promises) {
    return new Promise((resolve, reject) => {
        const promiseArray = Array.from(promises);

        // 如果输入数组为空,根据规范,返回的 Promise 应该永远处于 Pending 状态
        // 这里的实现是,如果没有 promise 可迭代,就不会执行任何 resolve/reject
        // 从而保持 Promise 处于 Pending 状态。
        if (promiseArray.length === 0) {
            return;
        }

        // 遍历输入数组中的每个 Promise
        promiseArray.forEach(promise => {
            // 同样,将当前项包装成 Promise,以处理非 Promise 值
            Promise.resolve(promise)
                .then(value => {
                    // 只要有任何一个 Promise 成功,就立即解决 myPromiseRace 返回的 Promise
                    // 后续的 Promise 即使成功或失败,也不会影响已解决的状态
                    resolve(value);
                })
                .catch(error => {
                    // 只要有任何一个 Promise 失败,就立即拒绝 myPromiseRace 返回的 Promise
                    // 后续的 Promise 即使成功或失败,也不会影响已解决的状态
                    reject(error);
                });
        });
    });
}

// 测试 myPromiseRace
console.log("n--- myPromiseRace 示例 ---");

const raceTask1 = new Promise(resolve => setTimeout(() => resolve("Race Task 1 (slow)"), 300));
const raceTask2 = new Promise(resolve => setTimeout(() => resolve("Race Task 2 (medium)"), 100));
const raceTask3 = new Promise((_, reject) => setTimeout(() => reject(new Error("Race Task 3 (fast failure)")), 50));
const raceTask4 = "Immediate Value";

myPromiseRace([raceTask1, raceTask2, raceTask3, raceTask4])
    .then(value => {
        console.log("myPromiseRace 成功:", value); // "Immediate Value"
    })
    .catch(error => {
        console.error("myPromiseRace 失败:", error.message);
    });

myPromiseRace([raceTask1, raceTask2, raceTask3])
    .then(value => {
        console.log("myPromiseRace 成功 (不应执行):", value);
    })
    .catch(error => {
        console.error("myPromiseRace 失败:", error.message); // "Race Task 3 (fast failure)"
    });

console.log("myPromiseRace 空数组测试 (不会有输出):");
myPromiseRace([]); // 应该不会有任何 resolve 或 reject

myPromiseRace 实现思路详解:

  1. 返回新 Promise: 和 myPromiseAll 一样,返回一个新的 Promise。
  2. 处理空数组: 如果输入数组为空,Promise.race 的规范是返回的 Promise 永远不解决。通过简单地 return 而不调用 resolvereject,我们的实现也符合这一点。
  3. 迭代并监听:
    • 遍历 promiseArray 中的每一个元素。
    • 使用 Promise.resolve(promise) 确保处理的是 Promise 实例。
    • 对每个 Promise 附加 .then(resolve).catch(reject)
    • 这里的关键在于,外部 Promise 的 resolvereject 函数直接作为内部 Promise 的处理函数。这意味着第一个调用 resolvereject 的 Promise 将立即改变外部 Promise 的状态,而之后其他 Promise 的状态变化将不再有任何影响,因为 Promise 的状态一旦改变就不可逆转。

深入并发请求控制:限制并发数

Promise.allPromise.race 固然强大,但它们都假定所有输入的 Promise 可以同时启动。在许多实际场景中,这种无限制的并发可能会带来问题:

  • 服务器负载: 同时发起大量请求可能导致服务器过载,甚至触发限流。
  • 客户端资源: 大量的网络连接、内存占用可能影响浏览器性能。
  • API 限制: 许多第三方 API 对并发请求有严格限制。

因此,我们需要一种机制来限制并发请求的数量。这通常通过实现一个“Promise 池”或“并发队列”来完成。

实现 promisePool (或 promiseLimit)

我们的目标是创建一个函数,它接受一个任务列表和一个并发限制数,然后确保在任何给定时间,只有不超过限制数的任务在执行。

设计思路:

  1. 任务队列: 存储所有待执行的任务。为了控制执行时机,任务通常以返回 Promise 的函数形式提供。
  2. 正在运行的任务: 跟踪当前正在执行的任务。
  3. 结果收集: 像 Promise.all 一样,我们需要收集所有任务的结果并按顺序返回。
  4. 递归/循环调度: 当一个任务完成时,检查是否可以启动下一个任务。
/**
 * 实现一个 Promise 并发池,控制同时执行的 Promise 数量
 * @param {Array<Function>} tasks - 任务数组,每个任务是一个返回 Promise 的函数
 * @param {number} limit - 最大并发数
 * @returns {Promise<Array<any>>} - 返回一个 Promise,当所有任务完成时解决,包含所有结果
 */
function promisePool(tasks, limit) {
    const results = []; // 存储所有任务的结果
    const running = []; // 存储当前正在执行的 Promise 实例
    let index = 0;      // 当前处理到的任务索引

    return new Promise((resolve, reject) => {
        // 启动下一个任务的调度函数
        const runNext = () => {
            // 如果所有任务都已处理完毕,并且没有任务正在运行,则解决主 Promise
            if (index >= tasks.length && running.length === 0) {
                resolve(results);
                return;
            }

            // 只要还有任务待启动,并且当前运行的任务数量未达到限制
            while (running.length < limit && index < tasks.length) {
                const currentTaskIndex = index; // 记录当前任务在原始任务列表中的索引
                const taskFunction = tasks[index++]; // 获取下一个任务函数并递增索引

                // 执行任务,并处理其结果
                const promise = Promise.resolve(taskFunction()).then(val => {
                    results[currentTaskIndex] = val; // 将结果存储在对应索引位置
                }).catch(err => {
                    // 任何一个任务失败,整个 promisePool 就失败
                    reject(err);
                    // 返回一个拒绝的 Promise,确保这个链条上的后续 then/catch 不会意外执行
                    // 并且在 running 数组中,这个 promise 仍然是拒绝状态,直到 finally
                    return Promise.reject(err);
                }).finally(() => {
                    // 无论成功或失败,任务完成,将其从 running 数组中移除
                    const runningPromiseIndex = running.indexOf(promise);
                    if (runningPromiseIndex !== -1) {
                        running.splice(runningPromiseIndex, 1);
                    }
                    // 任务完成后,尝试调度下一个任务
                    runNext();
                });
                running.push(promise); // 将新启动的 Promise 加入 running 数组
            }
        };

        // 首次调用,启动任务池
        runNext();
    });
}

// 辅助函数:创建一个模拟异步任务
const createAsyncTask = (id, delay) => () => {
    console.log(`[Pool] Task ${id} starting...`);
    return new Promise(resolve => {
        setTimeout(() => {
            console.log(`[Pool] Task ${id} completed after ${delay}ms`);
            resolve(`Result of Task ${id}`);
        }, delay);
    });
};

console.log("n--- Promise 并发池示例 (limit = 2) ---");

const poolTasks = [
    createAsyncTask(1, 1000),
    createAsyncTask(2, 500),
    createAsyncTask(3, 1200),
    createAsyncTask(4, 300),
    createAsyncTask(5, 800),
    createAsyncTask(6, 700),
];

promisePool(poolTasks, 2) // 限制同时运行的任务为2个
    .then(allResults => {
        console.log("All tasks in pool completed:", allResults);
    })
    .catch(error => {
        console.error("A task in pool failed:", error.message);
    });

// 模拟一个失败的任务
const failingPoolTask = (id, delay) => () => {
    console.log(`[Pool] Failing Task ${id} starting...`);
    return new Promise((_, reject) => {
        setTimeout(() => {
            console.log(`[Pool] Failing Task ${id} rejected after ${delay}ms`);
            reject(new Error(`Error from Task ${id}`));
        }, delay);
    });
};

const poolTasksWithError = [
    createAsyncTask(7, 600),
    failingPoolTask(8, 200), // 这个任务会失败
    createAsyncTask(9, 800),
];

promisePool(poolTasksWithError, 2)
    .then(allResults => {
        console.log("All tasks in pool with error completed:", allResults);
    })
    .catch(error => {
        console.error("A task in pool with error failed:", error.message); // Error from Task 8
    });

promisePool 实现思路详解:

  1. results 数组: 存储所有任务的最终结果,通过任务的 currentTaskIndex 确保顺序。
  2. running 数组: 这是一个关键的数据结构,它维护了当前正在执行的所有 Promise 实例。我们通过 running.length 来判断是否达到了并发限制。
  3. index 变量: 指向 tasks 数组中下一个待启动的任务。
  4. runNext() 调度函数: 这是整个并发池的核心。
    • 结束条件: 当 index 达到 tasks.length (所有任务都已启动) 且 running.length 为 0 (所有已启动的任务都已完成) 时,说明所有工作都已完成,此时 resolve(results)
    • 启动新任务: 在 while 循环中,只要还有待启动的任务 (index < tasks.length) 并且并发数未达到限制 (running.length < limit),就从 tasks 数组中取出下一个任务函数并执行它。
    • 任务处理: 每个任务函数返回一个 Promise。我们对其链式调用 .then().catch().finally()
      • .then(): 存储任务的成功结果到 results 数组的正确位置。
      • .catch(): 如果任何一个任务失败,整个 promisePool 应该立即失败,所以我们调用外部 reject(err)。同时返回 Promise.reject(err) 是为了阻止这个 Promise 链的后续 .then() 被调用,并确保 finally 块能被执行。
      • .finally(): 无论任务成功或失败,都表示它已完成。此时,将该 Promise 从 running 数组中移除,并递归调用 runNext()。这一步至关重要,它“腾出”了一个并发槽位,允许调度器尝试启动下一个任务。
    • 添加到 running: 将新启动的 Promise 添加到 running 数组中,以便跟踪其状态。
  5. 初始启动: 第一次调用 runNext() 会启动首批 limit 个任务。

精准的超时控制:避免无限等待

在异步编程中,我们经常面临网络请求超时、长时间计算无响应等问题。如果一个操作一直处于 Pending 状态,不仅会阻塞后续逻辑,还会消耗资源,甚至导致应用假死。这时,超时控制就显得尤为重要。

Promise.race 是实现超时控制的理想工具,因为它能让我们将一个实际任务 Promise 与一个定时器 Promise 进行“赛跑”。

实现 withTimeout 函数

我们将创建一个通用的 withTimeout 函数,它可以包裹任何 Promise,并为其添加超时机制。

// 定义一个自定义错误类型,使超时错误更具识别性
class TimeoutError extends Error {
    constructor(message = "Operation timed out") {
        super(message);
        this.name = "TimeoutError"; // 设置错误名称
    }
}

/**
 * 为一个 Promise 添加超时机制
 * @param {Promise<any>} promise - 要添加超时控制的原始 Promise
 * @param {number} ms - 超时时间,单位毫秒
 * @param {string} [errorMessage] - 可选的超时错误消息
 * @returns {Promise<any>} - 返回一个新的 Promise,它将在原始 Promise 解决或超时时解决/拒绝
 */
function withTimeout(promise, ms, errorMessage) {
    // 创建一个超时 Promise
    const timeout = new Promise((_, reject) => {
        // 设置一个定时器
        const id = setTimeout(() => {
            // 定时器触发时,清除自身,并拒绝超时 Promise
            clearTimeout(id);
            reject(new TimeoutError(errorMessage || `Operation timed out after ${ms}ms`));
        }, ms);
    });

    // 使用 Promise.race 竞争原始 Promise 和超时 Promise
    // 哪个先解决(成功或失败),就采纳哪个的结果
    return Promise.race([promise, timeout]);
}

// 辅助函数:模拟一个会延迟的异步数据获取
const fetchData = (id, delay) => new Promise(resolve => {
    console.log(`[Timeout] Fetching data ${id} (expected delay: ${delay}ms)...`);
    setTimeout(() => {
        console.log(`[Timeout] Data ${id} fetched after ${delay}ms.`);
        resolve(`Data ${id} after ${delay}ms`);
    }, delay);
});

console.log("n--- 超时控制示例 ---");

// 场景1: 任务在超时前完成
withTimeout(fetchData(1, 800), 1000, "Fetch 1 timed out!")
    .then(data => console.log("[Timeout Success]:", data)) // Data 1 after 800ms
    .catch(error => console.error("[Timeout Error]:", error.message));

// 场景2: 任务超时
withTimeout(fetchData(2, 1200), 1000, "Fetch 2 took too long!")
    .then(data => console.log("[Timeout Success]:", data))
    .catch(error => console.error("[Timeout Error]:", error.message)); // Fetch 2 took too long!

// 场景3: 任务自身失败,但在超时前
const failingFetch = new Promise((_, reject) => {
    console.log("[Timeout] Failing fetch starting...");
    setTimeout(() => {
        console.log("[Timeout] Failing fetch rejected.");
        reject(new Error("Network connection lost!"));
    }, 500);
});

withTimeout(failingFetch, 1000, "Failing fetch timed out!")
    .then(data => console.log("[Timeout Success]:", data))
    .catch(error => console.error("[Timeout Error]:", error.message)); // Network connection lost!

withTimeout 实现思路详解:

  1. 自定义 TimeoutError: 创建一个继承自 Error 的自定义错误类 TimeoutError,这有助于在 .catch() 块中区分是哪种类型的错误(是业务逻辑错误还是超时错误)。
  2. 创建超时 Promise:
    • timeout 是一个新的 Promise。
    • 在它的执行器中,我们设置了一个 setTimeout 定时器。
    • 当定时器触发时(即达到 ms 毫秒),我们调用 reject() 来拒绝这个 timeout Promise,并附带一个 TimeoutError
    • clearTimeout(id) 是一个好习惯,可以在超时发生后立即清理定时器,避免潜在的资源泄露或不必要的执行。
  3. Promise.race 组合:
    • Promise.race([promise, timeout]) 是实现超时控制的核心。
    • 如果原始 promisems 毫秒内解决了(无论是成功还是失败),那么 Promise.race 就会采纳 promise 的结果。
    • 如果 ms 毫秒到了,timeout Promise 先拒绝了,那么 Promise.race 就会采纳 timeout Promise 的拒绝结果,即抛出 TimeoutError

Promise.all 与超时控制的结合

我们可以将 withTimeout 应用到 Promise.all 中的每一个任务上,从而为每个并发请求设置独立的超时时间。

console.log("n--- Promise.all 结合超时控制示例 ---");

const taskA = fetchData('A', 800);
const taskB = fetchData('B', 1500); // 这个任务会超时
const taskC = fetchData('C', 500);

const tasksWithIndividualTimeouts = [
    withTimeout(taskA, 1000, "Task A timed out"),
    withTimeout(taskB, 1000, "Task B timed out"), // B 任务需要 1500ms,但超时设置为 1000ms
    withTimeout(taskC, 2000, "Task C timed out"),
];

Promise.all(tasksWithIndividualTimeouts)
    .then(results => {
        console.log("All tasks (with timeouts) completed:", results);
    })
    .catch(error => {
        console.error("A task (with timeouts) failed or timed out:", error.message);
        // 输出: A task (with timeouts) failed or timed out: Task B timed out
    });

在这个例子中,taskB 将会在 1000ms 时触发其超时,导致 Promise.all 立即拒绝,即使 taskAtaskC 最终会成功。这展示了如何通过组合使用这些工具来构建健壮的异步逻辑。


Promise.allPromise.race 关键区别总结

为了更好地理解何时使用哪个工具,我们来总结一下它们的关键区别:

特性 Promise.all Promise.race
输入 接受一个可迭代对象(如数组)的 Promise 接受一个可迭代对象(如数组)的 Promise
输出 返回一个 Promise 返回一个 Promise
成功条件 所有输入 Promise 都成功(fulfilled) 第一个输入 Promise 结算(fulfilled 或 rejected)
失败条件 任何一个输入 Promise 失败(rejected) 第一个输入 Promise 结算(fulfilled 或 rejected)
成功值 一个数组,包含所有输入 Promise 的成功值,顺序与输入一致 第一个成功 Promise 的值
失败值 第一个失败 Promise 的错误 第一个失败 Promise 的错误
空数组输入 立即成功,返回空数组 [] 永不结算(保持 pending 状态)
主要用途 等待多个并行任务全部完成,收集所有结果 实现超时控制,获取多个任务中的最快响应
并发行为 所有输入 Promise 同时开始执行 所有输入 Promise 同时开始执行

异步编程的基石与进阶

通过本次讲座,我们不仅深入理解了 Promise.allPromise.race 这两个 JavaScript 异步编程的基石,更通过手写实现加深了对其内部机制的掌握。在此基础上,我们进一步探讨了如何利用这些工具构建更高级的并发控制(如限制并发数)和超时控制机制。

掌握这些模式对于编写高性能、高可用且易于维护的异步代码至关重要。希望大家能够将这些知识应用到实际项目中,不断探索和实践,成为更优秀的 JavaScript 开发者。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注