JavaScript 并行 Fetch 与 Promise.all:用 async/await 批量获取 GitHub 用户(en.javascript.info 实战习题解析)

发布时间:2026/10/8 1:50:16
JavaScript 并行 Fetch 与 Promise.all:用 async/await 批量获取 GitHub 用户(en.javascript.info 实战习题解析)
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载本篇文章围绕 en.javascript.infoModern JavaScript Tutorial网络请求章节中的一道经典实战习题展开编写异步函数getUsers(names)对一组 GitHub 用户名并发发起fetch请求批量获取用户信息并在任意请求失败或用户不存在时以null占位。读完本文你将掌握fetch的两阶段响应模型、如何在async/await风格下灵活穿插低层 Promise API 实现并发、以及如何用Promise.all汇聚结果并做优雅降级——这些都是真实项目中批量拉取远程数据的必备技能。题目要求并发、独立、容错习题原文位于 task.md其核心要求如下实现异步函数getUsers(names)接收一个 GitHub 登录名数组对每个用户名USERNAME调用 GitHub 公开 APIhttps://api.github.com/users/USERNAME每个用户恰好发起一次fetch请求请求之间互不等待以便数据尽快全部到达任何请求失败或用户不存在时在结果数组中对应位置返回null。仓库在_js.view/沙箱目录中提供了配套的练习模板 source.js 与自动化测试 test.js读者可以先尝试自行实现再对照下文解答。标准解答逐行拆解习题官方解答位于 solution.md实现代码与沙箱中的 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; }这段代码虽然只有二十余行却精准地踩中了fetch并发编程的几个关键点下面逐一展开。第一步为每个名字独立创建一个任务for...of循环遍历names对每个名字立即调用fetch(...)。fetch由浏览器自动发起并且立刻返回一个 Promise不会阻塞循环继续执行。因此 N 个请求在循环期间就已全部发出请求之间天然并行满足互不等待、尽快到达的要求。每个fetch返回的 Promise 被存入jobs数组供后续统一收敛let job fetch(https://api.github.com/users/${name}).then(...); jobs.push(job);第二步用.then的第二参数统一处理网络失败这里用到了Promise.then(onFulfilled, onRejected)的双参数形式第一个回调处理成功响应服务器返回了 HTTP 状态码第二个回调failResponse null处理拒绝网络不通、域名不存在、请求被中断等。对照 fetch 章节正文 中明确说明的语义fetch返回的 Promise只在无法发出 HTTP 请求时 reject如网络故障而 404、500 等异常 HTTP 状态码并不会导致 reject它们依然走成功分支。因此网络层面的失败 → 由.then的第二个回调捕获返回null业务层面的失败如用户不存在返回 404→ 落入第一个回调靠检查successResponse.status ! 200拦截同样返回null。两条路径都归一到null正是题目任何请求失败都返回 null的完整实现。第三步状态码检查与 JSON 解析对于成功响应先检查statusif (successResponse.status ! 200) { return null; } else { return successResponse.json(); }GitHub API 在用户存在时返回200和 JSON 体用户名不存在时返回404或其他非 200 状态此时直接return null避免对错误页面做无意义的 JSON 解析。这里需要理解fetch的两阶段模型详见 5-network/01-fetch/article.md第一阶段fetch的 Promise 在服务器响应头到达时即 resolve此时可读取response.status、response.ok、response.headers但响应体尚未下载完成第二阶段调用response.json()或.text()、.blob()、.arrayBuffer()、.formData()读取并解析响应体它返回另一个 Promise。response.json()解析完成后返回的是真正的 JS 对象GitHub 用户数据它作为.then回调的返回值被展平进job这个 Promise最终jobs数组里每个元素都是一个已经解析好用户对象或null的 Promise。核心设计为什么.json()要挂在每个fetch上而不是等Promise.all之后再调这是本解答最值得玩味的工程细节原文也特意用一整段强调.then调用直接挂在fetch上这样当某个响应到达时它不必等待其他请求而是立即开始读取.json()。对比一种看起来更 async/await 风格的写法// 反面示例先等所有响应再逐个解析 JSON let responses await Promise.all(names.map(name fetch(https://api.github.com/users/${name}))); let results responses.map(response response.status 200 ? response.json() : null);这种写法的问题是fetch的 Promise 只代表响应头到达不代表响应体下载完成。Promise.all会等待所有请求的响应头都到达后才放行然后才开始逐一调用.json()下载各个响应体——大批量请求时尾部慢请求会拖住整个流程JSON 体下载的并行度也大打折扣。而官方解答的做法是每个fetch一得到响应头立刻就地开始response.json()的读取与解析job这个 Promise 直接代表该用户的完整数据。Promise.all(jobs)等待的是全部用户数据解析完毕而不是全部响应头到达这一较浅的阶段。原文对此的总结非常到位这正是主要使用 async/await、但低层 Promise API 依然有用的典型例子。反直觉但正确的提示response.json()返回的是一个 Promise而.then回调返回 Promise 时会被自动展平assimilate所以job最终 resolve 的值是解析后的用户对象本身而不是嵌套的 Promise。这正是我们可以把读取解析提前到每个请求上的语法基础。Promise.all并发任务的收敛与顺序保持let results await Promise.all(jobs); return results;Promise.all接受一个 Promise 数组返回一个新 Promise全部成功resolve 为一个数组元素顺序与输入数组一一对应——这正是题目要求的结果数组与 names 一一对应包括null占位得以成立的保证与请求完成顺序无关任一失败整体 reject。由于本解答中每个job都已经把失败翻译成了nulljob永远 resolve 而非 rejectPromise.all永远不会因为单个用户请求失败而整体抛出异常天然具备容错性。仓库中的测试 test.js 精确验证了这一行为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); }); });三个断言分别验证真实用户iliakan、remy被成功解析为对象并能读取.login不存在的用户名no.such.users对应的结果位置为null。测试用例与解答实现形成闭环可以直接在本地浏览器沙箱中运行验证。扩展思考几个值得注意的边界与变体1. 为什么用status ! 200而非response.okresponse.ok对 200–299 的所有状态码都为true。对 GitHub 用户查询而言200是唯一命中状态用status ! 200更严格若你希望宽松接受 2xx 家族改用if (!response.ok) return null;亦可语义等价性取决于 API 的实际约定。2. 请求数较大时的并发控制当names数组很大时一次性并发数百个请求可能触发 GitHub API 的限流rate limit或让浏览器连接池过载。此时可将本解答的任务数组 Promise.all模式扩展为分批chunk或滑动窗口限流但需注意一旦引入串行等待就牺牲了本题互不等待的初衷应根据实际场景权衡。3. 低层 Promise 与 async/await 的混用原则从 solution.md 可以看出getUsers本身是async函数内部却用for...of.then而非await——这正是收集阶段用循环 Promise收敛阶段用 await的经典模式。与之相对的逐个await写法会退化为串行请求// 反例串行请求违反互不等待 let results []; for (let name of names) { let res await fetch(https://api.github.com/users/${name}); results.push(res.status 200 ? await res.json() : null); } return results;4. 复用与封装jobs数组 Promise.all的骨架可推广到任何批量、并发、逐项容错的场景例如批量查询商品价格、批量拉取仓库元数据、批量校验链接可用性等。只需替换fetch的 URL 模板与成功/失败分支的取值逻辑即可。小结fetch是两阶段操作先拿到响应头可读status/ok再通过response.json()等方法读取响应体网络失败Promise reject与非 200 状态如 404是两类不同的失败需要分别处理最终都可归一为null把.then(...)直接挂在每个fetch上、让job代表完整数据再用Promise.all收敛能最大化请求与 JSON 解析的并行度Promise.all保持结果顺序与输入一致是并发发起、按序返回的标准答案。想继续深入的同学可以顺着 fetch 章节 学习请求头headers、POST请求体、二进制上传Blob等更多fetch能力并在 task.md 的沙箱里动手完成本题再用 test.js 自测。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐Next.js App Router 并行数据获取实战用 Promise.all 替代串行 await基于 Next.js 仓库 no-serial-await 评测案例Next.js App Router 并行数据获取实战用 Promise.all 替代串行 await基于 Next.js 仓库 no serial awa前端后端Web框架SSR前端构建用 fetch 并发拉取多个 GitHub 用户Modern JavaScript Tutorial 的 getUsers 任务全解析用 fetch 并发拉取多个 GitHub 用户Modern JavaScript Tutorial 的 getUsers 任务全解析 在 Web 前端开发中文档/教程前端JavaScript Fetch 下载进度追踪实战基于 response.body 流式读取实现en.javascript.info 教程深度解析JavaScript Fetch 下载进度追踪实战基于 response.body 流式读取实现en.javascript.info 教程深度解析 在现代文档/教程前端上一篇Detekt 静态代码分析实战用 Kotlin 代码质量守卫筑牢 Android 开发路线图中的工程底线下一篇XUnity Auto Translator如何让Unity游戏轻松实现多语言翻译创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考