为什么 Promise.all() 比顺序执行 await 调用更快?
Promise.all() 版本通常更快,因为 /todos 和 /users 互不依赖,两个请求的等待时间可以重叠。顺序版本要等 /todos 的响应体读取和 JSON 解析都完成后,才开始请求 /users。
速度差异取决于请求何时启动。发起请求的是 fetch(),Promise.all() 负责汇总结果。
两种写法的时间花在哪里?
顺序版本按以下流程执行:
请求 todos → 读取并解析 todos → 请求 users → 读取并解析 users → 合并数据
执行到第一个 await 时,当前异步函数暂停,等对应的 Promise 兑现后才继续。此时,后面的 fetch('/users') 还没有被调用。
并发版本会先求值数组中的各个元素:
const [todosRes, usersRes] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/todos'),
fetch('https://jsonplaceholder.typicode.com/users')
]);
具体顺序是:
- 调用第一个
fetch(),启动 todos 请求,得到一个 Promise。 - 紧接着调用第二个
fetch(),启动 users 请求,得到另一个 Promise。 Promise.all()接收这些 Promise,等待它们全部成功。await暂停当前函数,等结果准备好后继续执行。
两个 fetch() 仍然先后调用,但第二个调用无须等第一个请求完成。这里的“并发”指请求进行的时间可以重叠,不保证网络数据在同一时刻发出。
假设两项独立任务分别耗时 T₁、T₂,并发时也没有明显的资源争用,总耗时可以粗略估算为:
顺序执行:T ≈ T₁ + T₂
并发执行:T ≈ max(T₁, T₂)
这个估算适用于主要耗时在网络等待上的任务。连接限制、带宽共享、服务端负载和同步计算都会影响实际耗时,因此并发不保证每次都更快。
JavaScript 如何调度这些请求?
在浏览器中,JavaScript 调用 fetch() 后,浏览器的网络设施会处理网络操作,无须让 JavaScript 持续占用主线程等待服务器返回数据。因此,单线程 JavaScript 也能处理并发网络请求。
执行到 await 时,当前异步函数暂停,把执行权交回调用方。调用栈清空后,浏览器可以继续处理事件和其他任务,也有机会进行渲染。
被等待的 Promise 落定后,await 后面的代码会通过微任务恢复执行:
- Promise 兑现时,
await得到结果,继续往下执行。 - Promise 拒绝时,
await抛出对应的错误,可以由try...catch捕获。
Promise.all() 不会自动让普通 JavaScript 计算并行执行。把耗时循环放进 Promise,也不会因此用上多个 CPU 核心。浏览器中的 CPU 密集型计算可以考虑使用 Web Workers。
原来的并发版本还能怎样调整?
原代码把两个 fetch() 放进了 Promise.all(),但读取 JSON 时仍然逐个等待:
const todos = await todosRes.json();
const users = await usersRes.json();
只有 todosRes.json() 完成后,才会调用 usersRes.json()。
fetch() 和 response.json() 完成的工作不同:fetch() 通常在响应状态和响应头可用时兑现,此时响应体可能还没下载完;response.json() 则会读取完整响应体,再将其解析为 JavaScript 值。
可以把请求、检查 HTTP 状态和读取 JSON 封装成一项任务,再并发等待两项任务。这样,每个响应一旦可用,就能开始读取自己的响应体。
const BASE_URL = 'https://jsonplaceholder.typicode.com';
async function getJSON(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`请求失败:${response.status} ${url}`);
}
return response.json();
}
async function getTodo2() {
const [todos, users] = await Promise.all([
getJSON(`${BASE_URL}/todos`),
getJSON(`${BASE_URL}/users`)
]);
todos.forEach(todo => {
todo.user = users.find(user => user.id === todo.userId);
});
return todos;
}
Promise.all() 按输入顺序返回结果,即使 /users 先完成,解构得到的第一个结果仍然是 todos。
这里重叠的是异步操作的等待时间,两份 JSON 的解析不一定会在不同线程上并行执行。
连续写两个 await,一定会顺序请求吗?
这取决于请求在哪里启动。下面的代码连续使用了两个 await,但在执行第一个 await 之前,两个 fetch() 就已经调用,请求仍然可以并发进行:
const todosPromise = fetch(`${BASE_URL}/todos`);
const usersPromise = fetch(`${BASE_URL}/users`);
const todosResponse = await todosPromise;
const usersResponse = await usersPromise;
等待一组任务时,使用 Promise.all() 通常更清楚,也方便统一处理它们的拒绝。如果提前启动多个 Promise,再逐个等待,后面的 Promise 可能在代码仍等待前一个时就已拒绝,造成暂时未被处理的拒绝。
哪些情况下更适合顺序 await?
后一个操作依赖前一个结果
例如,先取得待办事项,才能知道要查询哪个用户:
const todo = await getJSON(`${BASE_URL}/todos/1`);
const user = await getJSON(`${BASE_URL}/users/${todo.userId}`);
第二个请求的 URL 依赖 todo.userId,必须等待第一个结果。原问题中的 /todos 和 /users 地址都已知,没有这层依赖。
操作必须遵守业务顺序
例如,先创建记录,再用返回的 ID 上传附件;或者前一步成功后,才允许执行下一步。顺序 await 可以直接表达这些约束。
有先后要求的写操作如果为了节省时间改成并发,可能改变业务结果。
需要控制请求数量或资源占用
处理大量请求时,一次性全部启动可能增加服务端压力,也可能占用过多连接和内存。
逐个 await 可以把并发数限制为 1。数据量较大时,也可以使用有限并发,让同时处理的任务保持在固定数量内。
如果接口限制的是“每秒请求次数”,仅靠顺序执行未必够用,还需要按接口规则安排请求间隔。
使用时还要注意什么?
Promise.all() 中任意一项拒绝,汇总得到的 Promise 就会拒绝,已经启动的其他请求却不会自动取消。需要主动取消 fetch() 时,可以使用 AbortController。需要收集每项任务的成功或失败结果时,可以使用 Promise.allSettled()。
fetch() 遇到 HTTP 404、500 等响应时通常仍会兑现,所以示例中检查了 response.ok。网络错误和 JSON 解析错误也需要在调用处处理:
showToDos().catch(error => {
console.error('加载待办事项失败:', error);
});
HTML 中还可以给外部脚本添加 defer,保证脚本执行时文档已经解析完成:
<script src="app.js" defer></script>
这样,调用 document.getElementById('todos') 时,后面的 <ul> 已经解析完成,无须靠请求耗时来等待它出现。列表创建和事件绑定的逻辑本身不影响两个请求能否并发。
备注:内容仅供参考。
