各位编程爱好者,大家好!
今天,我们将深入探讨 JavaScript 异步编程的核心机制,特别是如何高效地处理并发请求与实现精妙的超时控制。在现代 Web 应用中,异步操作无处不在,从数据抓取到用户界面的响应,它们构成了用户体验的基石。Promises 作为 JavaScript 异步编程的基石,为我们提供了一种结构化、可管理的方式来处理这些操作。
然而,单个 Promise 往往不足以应对复杂的场景。想象一下,你需要同时从多个 API 端点获取数据,或者你需要从多个镜像服务器中选择响应最快的一个,再或者你需要确保某个操作在限定时间内完成。这时,Promise.all 和 Promise.race 就闪亮登场了。它们是 Promise API 提供的两个强大工具,用于协调多个 Promise 的行为。
本次讲座的目标是:
- 深入理解
Promise.all和Promise.race的工作原理和常见用例。 - 亲手实现 它们的核心逻辑,从而加深对 Promise 内部机制的理解。
- 拓展应用 学习如何利用这些基础构建更高级的并发控制(如限制并发数)和超时控制机制。
让我们从 Promises 的基础回顾开始,为后续的深入讨论打下坚实的基础。
理解 JavaScript Promises:异步的契约
在深入 Promise.all 和 Promise.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 的核心行为
- 输入: 接受一个可迭代对象(通常是一个数组),其中包含多个 Promise 实例。
- 输出: 返回一个新的 Promise 实例。
- 成功条件: 只有当输入数组中的所有 Promise 都成功(fulfilled)时,
Promise.all返回的 Promise 才会成功。- 成功时,它会返回一个数组,该数组包含了所有输入 Promise 的成功值,并且这些值的顺序与输入 Promise 的顺序严格一致。
- 失败条件: 只要输入数组中有一个 Promise 失败(rejected),
Promise.all返回的 Promise 就会立即失败。- 失败时,它会返回第一个失败 Promise 的错误信息。
- 非 Promise 值: 如果输入数组中包含非 Promise 的值(例如普通数字、字符串),
Promise.all会将其视为一个已经成功(fulfilled)的 Promise,并直接使用该值。 - 空数组: 如果输入数组为空,
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 实现思路详解:
- 返回新 Promise:
myPromiseAll必须返回一个新的 Promise,因为它的状态和结果需要等待所有输入 Promise 的状态才能确定。 - 处理空数组: 这是
Promise.all的一个特殊规则。如果输入是一个空数组,它应该立即成功并返回一个空数组。 - 结果存储: 我们需要一个
results数组来按顺序存储每个成功 Promise 的值。重要的是要使用index来确保结果的顺序与输入 Promise 的顺序一致。 - 计数器:
completedCount变量用于跟踪已经完成(无论是成功还是失败)的 Promise 数量。 - 迭代并监听:
- 遍历
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 的核心行为
- 输入: 接受一个可迭代对象(通常是一个数组),其中包含多个 Promise 实例。
- 输出: 返回一个新的 Promise 实例。
- 结算条件: 只要输入数组中的任何一个 Promise 最先解决(settled,即变为 fulfilled 或 rejected),
Promise.race返回的 Promise 就会立即以该 Promise 的状态和值/错误为准进行结算。 - 非 Promise 值: 如果输入数组中包含非 Promise 的值,它会被视为一个立即解决的 Promise,并且很可能成为“最快”解决的 Promise。
- 空数组: 如果输入数组为空,
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 实现思路详解:
- 返回新 Promise: 和
myPromiseAll一样,返回一个新的 Promise。 - 处理空数组: 如果输入数组为空,
Promise.race的规范是返回的 Promise 永远不解决。通过简单地return而不调用resolve或reject,我们的实现也符合这一点。 - 迭代并监听:
- 遍历
promiseArray中的每一个元素。 - 使用
Promise.resolve(promise)确保处理的是 Promise 实例。 - 对每个 Promise 附加
.then(resolve)和.catch(reject)。 - 这里的关键在于,外部 Promise 的
resolve和reject函数直接作为内部 Promise 的处理函数。这意味着第一个调用resolve或reject的 Promise 将立即改变外部 Promise 的状态,而之后其他 Promise 的状态变化将不再有任何影响,因为 Promise 的状态一旦改变就不可逆转。
- 遍历
深入并发请求控制:限制并发数
Promise.all 和 Promise.race 固然强大,但它们都假定所有输入的 Promise 可以同时启动。在许多实际场景中,这种无限制的并发可能会带来问题:
- 服务器负载: 同时发起大量请求可能导致服务器过载,甚至触发限流。
- 客户端资源: 大量的网络连接、内存占用可能影响浏览器性能。
- API 限制: 许多第三方 API 对并发请求有严格限制。
因此,我们需要一种机制来限制并发请求的数量。这通常通过实现一个“Promise 池”或“并发队列”来完成。
实现 promisePool (或 promiseLimit)
我们的目标是创建一个函数,它接受一个任务列表和一个并发限制数,然后确保在任何给定时间,只有不超过限制数的任务在执行。
设计思路:
- 任务队列: 存储所有待执行的任务。为了控制执行时机,任务通常以返回 Promise 的函数形式提供。
- 正在运行的任务: 跟踪当前正在执行的任务。
- 结果收集: 像
Promise.all一样,我们需要收集所有任务的结果并按顺序返回。 - 递归/循环调度: 当一个任务完成时,检查是否可以启动下一个任务。
/**
* 实现一个 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 实现思路详解:
results数组: 存储所有任务的最终结果,通过任务的currentTaskIndex确保顺序。running数组: 这是一个关键的数据结构,它维护了当前正在执行的所有 Promise 实例。我们通过running.length来判断是否达到了并发限制。index变量: 指向tasks数组中下一个待启动的任务。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数组中,以便跟踪其状态。
- 结束条件: 当
- 初始启动: 第一次调用
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 实现思路详解:
- 自定义
TimeoutError: 创建一个继承自Error的自定义错误类TimeoutError,这有助于在.catch()块中区分是哪种类型的错误(是业务逻辑错误还是超时错误)。 - 创建超时 Promise:
timeout是一个新的 Promise。- 在它的执行器中,我们设置了一个
setTimeout定时器。 - 当定时器触发时(即达到
ms毫秒),我们调用reject()来拒绝这个timeoutPromise,并附带一个TimeoutError。 clearTimeout(id)是一个好习惯,可以在超时发生后立即清理定时器,避免潜在的资源泄露或不必要的执行。
Promise.race组合:Promise.race([promise, timeout])是实现超时控制的核心。- 如果原始
promise在ms毫秒内解决了(无论是成功还是失败),那么Promise.race就会采纳promise的结果。 - 如果
ms毫秒到了,timeoutPromise 先拒绝了,那么Promise.race就会采纳timeoutPromise 的拒绝结果,即抛出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 立即拒绝,即使 taskA 和 taskC 最终会成功。这展示了如何通过组合使用这些工具来构建健壮的异步逻辑。
Promise.all 与 Promise.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.all 和 Promise.race 这两个 JavaScript 异步编程的基石,更通过手写实现加深了对其内部机制的掌握。在此基础上,我们进一步探讨了如何利用这些工具构建更高级的并发控制(如限制并发数)和超时控制机制。
掌握这些模式对于编写高性能、高可用且易于维护的异步代码至关重要。希望大家能够将这些知识应用到实际项目中,不断探索和实践,成为更优秀的 JavaScript 开发者。