- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
在 Web 前端开发中,“一次性并发请求多个资源”是最常见的实战场景之一。本文基于 Modern JavaScript Tutorial(en.javascript.info)中 Fetch 章节 的经典任务getUsers,完整讲解如何用fetch+Promise.all批量、并发地从 GitHub API 拉取多个用户的公开信息,并正确处理网络失败与不存在的用户。读完本文,你将掌握async/await与底层 Promise API 混用的关键技巧,理解“何时该把.then直接挂在fetch上”这一并发优化的本质。
任务概述:从任务描述到实现目标
本任务源自仓库中的 任务描述文件,要求实现一个异步函数:
async function getUsers(names)- 输入:一个 GitHub 登录名(login)数组,例如
['iliakan', 'remy', 'no.such.users']; - 输出:一个与输入顺序对应的 GitHub 用户对象数组;
- 数据来源:单个用户的公开信息通过如下 REST 接口获取:
https://api.github.com/users/USERNAME其中USERNAME替换为具体的登录名。接口返回的是标准的 JSON 用户对象(含login、id、name、avatar_url等字段)。
任务对实现提出了三条必须同时满足的硬性要求:
- 每个用户恰好发起一次
fetch请求,不能复用、合并或重复请求; - 请求之间互不等待,数据要“尽可能快地到达”——即请求必须并发发出,而不是串行排队;
- 容错兜底:任何请求失败(如网络中断),或该用户不存在(HTTP 404),对应的结果位置必须返回
null,而不是让整个函数报错。
这三条要求分别对应并发度、延迟与健壮性三个维度,也是本任务真正的考察重点。
前置知识:fetch 的两阶段响应模型
在动手写代码之前,需要先厘清fetch的工作方式。如 Fetch 章节 所讲,一次fetch的响应过程分为两个阶段:
第一阶段:响应头到达。fetch(url)返回的 Promise 在服务器返回响应头时即告 resolve,此时可以读取 HTTP 状态码:
response.status—— HTTP 状态码,如200、404;response.ok—— 布尔值,status在 200–299 之间时为true。
注意:只有网络层失败(无网络、域名不存在等)才会导致fetch的 Promise reject;HTTP 404、500 这类异常状态码并不会 reject,而是以正常 resolve 的方式携带非 2xx 的状态码返回。
第二阶段:读取响应体。需要额外调用一个方法,例如response.json()把响应体解析为 JavaScript 对象。该方法本身也返回一个 Promise,在响应体读取并解析完成后 resolve。
因此典型的fetch用法是两次await:
let response = await fetch(url); // 第一阶段:拿到响应头与状态码 let user = await response.json(); // 第二阶段:读取并解析响应体这一“两阶段”模型是本任务解法的基石,尤其是第二阶段的response.json()何时开始执行,直接决定了请求是“并发读数据”还是“串行等结果”。
参考解法:完整代码与逐段拆解
仓库在 参考解法文档 和沙箱目录 _js.view/solution.js 中给出了标准答案,完整代码如下:
async function getUsers(names) { let jobs = []; for(let name of names) { let job = fetch(`https://api.github.com/users/${name}`).then( successResponse => { if (successResponse.status != 200) { return null; } else { return successResponse.json(); } }, failResponse => { return null; } ); jobs.push(job); } let results = await Promise.all(jobs); return results; }逐段拆解如下:
第一步:为每个名字创建一个“作业”Promise。用for..of遍历输入数组,对每个登录名拼接出对应 URL 并立即发起fetch:
let job = fetch(`https://api.github.com/users/${name}`).then(...);第二步:在.then中同时处理成功与失败两个分支。
.then( successResponse => { if (successResponse.status != 200) { return null; } else { return successResponse.json(); } }, failResponse => { return null; } )successResponse分支:当响应头到达时执行。若status != 200(包括 404 等),直接返回null;否则返回successResponse.json()—— 注意这里返回的是一个Promise,它会在响应体解析完成后 resolve 为用户对象;failResponse分支:对应fetch的 Promise 被 reject(网络层失败),返回null。
.then(onFulfilled, onRejected)双参数写法在这里很关键:它把“HTTP 非 200”和“网络失败”两条错误路径统一归一化为null值,避免了任何 Promise 处于 rejected 状态。
第三步:用Promise.all聚合所有作业。
let results = await Promise.all(jobs); return results;Promise.all接收 Promise 数组,返回一个新 Promise,在所有作业都 settle(无论成功失败)后 resolve 为一个数组,数组顺序与输入顺序一致。由于每个作业要么 resolve 成用户对象、要么 resolve 成null,Promise.all永远不会收到 rejection,因此不会因某个用户不存在而中断整个批量任务。
并发是核心:为什么把.then直接挂在fetch上
任务第二条要求“请求不应互相等待”,而解法文档特别强调了一个容易踩坑的写法差异。
先看“错误”的写法直觉:新手往往这样写:
let responses = await Promise.all(names.map(name => fetch(`https://api.github.com/users/${name}`))); let results = responses.map(response => response.json());这段代码虽然让fetch并发发出,但第二步对responses逐项调用response.json()时,每个response的响应体读取会等待所有fetch的响应头全部返回之后才开始。也就是说,Promise.all(names.map(...))这一行会先等待所有请求“响应头阶段”完成,才能进入“读响应体”阶段。响应体是数据的主体,这样整体上数据到达的时机被推迟了。
参考解法的精妙之处在于:response.json()的调用被直接放在每个fetch自己的.then回调里,响应头一到达就立刻开始读取该响应的响应体,无需等待其他任何请求:
let job = fetch(url).then(successResponse => successResponse.json());于是每个作业的“网络传输 + JSON 解析”是各自独立并行的:请求 A 的响应体还没读完时,请求 B 的响应体可能已经解析完成。最终Promise.all只负责收集结果,不负责串行化任何工作。
这正是解法文档末尾的结语所点明的主题:即使主力写法是async/await,底层 Promise API 依然有不可替代的用武之地。这里用到的知识点(then回调、Promise.all、Promise 的 resolve/reject 语义)都可以在仓库的 Promise 基础章节 与 Promise 链式调用章节 中找到完整理论支撑。
错误处理与边界情况
任务第三条要求所有失败场景统一输出null,参考解法覆盖了两种完全不同的失败来源:
| 失败来源 | 触发条件 | 处理位置 | 结果 |
|---|---|---|---|
| 网络层失败 | 断网、DNS 解析失败、请求被拒等 | fetch的 Promise reject,进入.then的第二个回调 | null |
| HTTP 非 200 | 用户不存在(404)、限流(403)等 | 响应头到达,successResponse.status != 200 | null |
两种路径都必须处理。特别提醒:HTTP 404 不会让fetchreject,如果只在.then的成功回调里返回response.json(),那么不存在的用户会得到一个 404 的响应体解析结果(通常是{"message": "Not Found", ...}之类),而不是null,从而破坏测试断言。因此显式检查status != 200是必不可少的一步。
此外,正因为每个作业内部已经把所有可能的 rejection 转换成了null,Promise.all才得以安全使用——否则任何一个用户的网络抖动都会让整个getUsers抛错。
沙箱与测试验证
仓库为本题提供了完整的可运行沙箱,位于 5-network/01-fetch/01-fetch-users/_js.view/ 目录:
- source.js —— 预置的题目骨架:只有
async function getUsers(names) { /* your code */ },留给读者实现; - solution.js —— 上文展示的参考解答;
- test.js —— 官方单元测试,使用 Mocha 断言:
describe("getUsers", function() { it("gets users from GitHub", async function() { let users = await getUsers(['iliakan', 'remy', 'no.such.users']); assert.equal(users[0].login, 'iliakan'); assert.equal(users[1].login, 'remy'); assert.equal(users[2], null); }); });测试用例精确验证了任务的三条要求:
users[0].login === 'iliakan'与users[1].login === 'remy':真实存在的用户被正确解析为 GitHub 用户对象;users[2] === null:不存在的用户no.such.users在结果数组的对应位置得到null;- 断言按数组下标进行,隐含地验证了结果顺序与输入顺序一致(这正是
Promise.all的保证)。
该测试同时涵盖了“成功”与“失败”两条路径,说明实现必须同时通过这两类断言才算正确。仓库中关于 Mocha 测试框架的完整用法,可参考 测试章节。
延伸:等价的map写法与更精细的错误处理
参考解法用for..of循环收集作业,逻辑清晰、便于逐行讲解。作为延伸,用map可以写得更紧凑,语义完全等价:
async function getUsers(names) { let results = await Promise.all( names.map(name => fetch(`https://api.github.com/users/${name}`).then( response => response.status == 200 ? response.json() : null, () => null ) ) ); return results; }两者本质相同:每个fetch自带.then,把异常统一收敛为null,再由Promise.all聚合。如果希望把“网络失败”与“HTTP 非 200”区分对待(例如前者重试、后者直接判空),可以分别使用.catch()与状态码检查,而参考解法用.then的双参数形式一次处理两条路径,代码更紧凑。
总结
getUsers是一个小而精的并发实战任务,它串起了fetch两阶段响应模型、.then回调、Promise.all聚合、async/await语法以及错误归一化这几大知识点:
- 每个用户一个
fetch,用for..of(或map)批量发起; .then直接挂在fetch上,让每个请求的响应体读取立即并行开始,这是满足“互不等待”要求的关键;status != 200返回null,覆盖“用户不存在”等 HTTP 层失败;- rejected 分支返回
null,覆盖网络层失败,并保证Promise.all永远不会收到 rejection; await Promise.all(jobs)保持结果顺序,等待全部作业完成后一次性返回。
理解了这个任务,你就掌握了在async/await时代仍然需要底层 Promise 技巧的核心场景:凡是要“并发发起、各自消化、统一聚合”的请求,都值得套用这一模式。更多fetch的选项(method、headers、body等)可继续阅读 Fetch 章节 与仓库中 网络请求章节索引 下的其他文章。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
将 JavaScript 提取为外部脚本:以 Modern JavaScript Tutorial 的 `alert` 任务为例
将 JavaScript 提取为外部脚本:以 Modern JavaScript Tutorial 的 alert 任务为例 在 Web 开发中,把 JavaS
文档/教程前端Modern JavaScript Tutorial 实战:用 `insertAdjacentHTML` 向列表插入 HTML —— "append-to-list" 任务全解析
Modern JavaScript Tutorial 实战:用 insertAdjacentHTML 向列表插入 HTML —— "append to list
文档/教程前端把10块钱的鼠标用出触控板的感觉:Mac Mouse Fix 完整上手指南
把10块钱的鼠标用出触控板的感觉:Mac Mouse Fix 完整上手指南 刚换了新 Mac,你第一句抱怨大概率是:这个鼠标真难用。滚动一格一格地跳,侧键按下去
文档/教程前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考