☰
用 fetch 并发拉取多个 GitHub 用户:Modern JavaScript Tutorial 的 getUsers 任务全解析
2026/10/8 13:06:40 网站建设 项目流程
  • 文档/教程
  • 前端

【免费下载链接】en.javascript.info

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

在 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等字段)。

任务对实现提出了三条必须同时满足的硬性要求:

  1. 每个用户恰好发起一次fetch请求,不能复用、合并或重复请求;
  2. 请求之间互不等待,数据要“尽可能快地到达”——即请求必须并发发出,而不是串行排队;
  3. 容错兜底:任何请求失败(如网络中断),或该用户不存在(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 != 200null

两种路径都必须处理。特别提醒: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

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询