ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

claude-skills 之 JavaScript 异步模式实战:Promise、并发控制、事件循环与取消机制

claude-skills 之 JavaScript 异步模式实战:Promise、并发控制、事件循环与取消机制 claude-skills 之 JavaScript 异步模式实战Promise、并发控制、事件循环与取消机制【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills本文围绕 claude-skills 仓库中 javascript-pro 技能的 async-patterns.md 参考文档展开系统讲解 Promise 创建与组合、async/await 最佳实践、错误处理策略、Promise 组合子、异步生成器、并发队列、事件循环、AbortController 取消机制与流式处理。读完本文你将掌握一套可直接复用的异步编程工具箱能够编写健壮、可控、无内存泄漏的现代 JavaScriptES2023异步代码。该文档是javascript-pro技能描述见 SKILL.md基于 ES2023 特性、async/await 模式、ESM 模块系统与 Node.js API 编写与审查代码的核心参考之一。在仓库中异步模式的实际调用贯穿始终——例如 assets/capture-screenshot.js 使用异步 IIFE 与 Promise 封装执行无头浏览器截图site/scripts/sync-content.mjs 以顶层main()顺序调度十余个文件同步阶段。下面逐层深入。Promise 创建与组合创建基础 PromisePromise 是 JavaScript 异步的基础抽象。最典型的工厂函数是把基于回调的 API如setTimeout包装为 Promise// Promise creation const delay (ms) new Promise(resolve setTimeout(resolve, ms)); const fetchWithTimeout (url, timeout 5000) { return Promise.race([ fetch(url), delay(timeout).then(() Promise.reject(new Error(Timeout))) ]); };这里展示了两个要点delay(ms)用new Promise包装定时器resolve在setTimeout触发后调用Promise.race让真实请求与超时倒计时赛跑倒计时先结束则整体 reject 一个Timeout错误——这是实现请求超时最简洁的方式。timeout默认 5000ms可传参调整。组合多个 Promise当多个请求之间存在先后依赖时需要按顺序组合// Promise composition const fetchUserData async (userId) { const user await fetch(/api/users/${userId}).then(r r.json()); const posts await fetch(/api/users/${userId}/posts).then(r r.json()); return { user, posts }; };注意此例中两个请求都依赖userId但彼此并不互相依赖——严格来说它可以用Promise.all并行化。原文保留它作为链式组合的语义演示await的求值顺序即执行顺序。若两个请求有真实依赖如第二个请求需要第一个的返回值这种串行写法才是正确的。async/await 最佳实践并行执行Promise.all当多个异步任务互不依赖时务必并行而非串行否则总耗时是各任务耗时之和// Parallel execution with Promise.all const fetchAllData async () { const [users, posts, comments] await Promise.all([ fetch(/api/users).then(r r.json()), fetch(/api/posts).then(r r.json()), fetch(/api/comments).then(r r.json()) ]); return { users, posts, comments }; };Promise.all并行发起所有请求全部成功才 resolve任一失败立即 rejectfail-fast解构赋值让结果保持清晰的语义对应若请求数巨大应配合后文的并发队列限制并发量避免瞬间创建海量连接。顺序执行保证依赖顺序// Sequential when order matters const processSteps async () { const step1 await executeStep1(); const step2 await executeStep2(step1); const step3 await executeStep3(step2); return step3; };每一步的输出是下一步的输入只能串行。await天然实现了依赖链且异常会在第一个失败处抛出并终止后续步骤。条件化并行// Conditional parallel execution const loadUserProfile async (userId, includeHistory false) { const userPromise fetchUser(userId); const settingsPromise fetchSettings(userId); const promises [userPromise, settingsPromise]; if (includeHistory) { promises.push(fetchHistory(userId)); } const [user, settings, history] await Promise.all(promises); return { user, settings, history }; };要点先发起 Promise调用异步函数再收集进数组而不是在await之后才发起。这样fetchUser与fetchSettings在进入Promise.all之前就已经并行启动。includeHistory为false时数组只有两项解构出的history为undefined调用方需自行处理。这与javascript-pro技能 SKILL.md 中MUST DO 使用 async/await 处理所有异步操作、遵循函数式编程原则的约束一致——用数据组合而非嵌套回调组织异步流。仓库中的真实异步调度仓库构建脚本 site/scripts/sync-content.mjs 的main()函数就是严格顺序 依赖传递的典型function main() { buildLinkMap(); cleanSyncedContent(); syncCoreDocs(); syncGuideDocs(); syncWorkflowDocs(); const skillIndex buildSkillIndex(); // 后续阶段依赖其返回值 syncSkillPages(skillIndex); cleanGeneratedPublicContent(); generateMarkdownMirrors(); generateIndexMirror(); generateLlmsTxt(); generateLlmsFullTxt(); } main();每个阶段处理完成后才会进入下一阶段skillIndex作为中间产物显式传递这正是async/await依赖链在真实工程中的映射该脚本以同步函数串联保证阶段顺序若阶段改为异步则应按processSteps的模式使用await。错误处理策略javascript-pro技能明确要求为所有异步函数实现 try/catch 错误处理见 SKILL.md 的 MUST DO / MUST NOT DO并给出了反例——未捕获的 rejection 与缺少空值守卫。分类捕获// Try-catch with specific error handling const safeApiCall async (url) { try { const response await fetch(url); if (!response.ok) { throw new Error(HTTP ${response.status}: ${response.statusText}); } return await response.json(); } catch (error) { if (error.name TypeError) { console.error(Network error:, error); } else if (error.name SyntaxError) { console.error(Invalid JSON:, error); } throw error; } };fetch只有在网络层面失败时才会 reject产生TypeErrorHTTP 4xx/5xx 并不会自动抛错因此必须检查response.okresponse.json()对非法 JSON 会抛SyntaxError依据error.name分类处理后重新抛出保留错误链路由上层决定是否吞掉。自定义错误类// Custom error classes class ApiError extends Error { constructor(status, message, data) { super(message); this.name ApiError; this.status status; this.data data; } } const fetchApi async (endpoint) { const response await fetch(endpoint); if (!response.ok) { const data await response.json().catch(() ({})); throw new ApiError(response.status, response.statusText, data); } return response.json(); };ApiError携带status与响应体data让调用方在catch里无需再次访问网络即可拿到完整诊断信息。.catch(() ({}))防止错误响应体解析失败时二次抛错。指数退避重试// Retry logic with exponential backoff const retryWithBackoff async (fn, maxRetries 3) { for (let i 0; i maxRetries; i) { try { return await fn(); } catch (error) { if (i maxRetries - 1) throw error; const delay Math.min(1000 * 2 ** i, 10000); await new Promise(resolve setTimeout(resolve, delay)); } } };默认最多重试 3 次maxRetries 3最后一次失败直接抛出不再等待等待时长按1000 * 2^i指数增长1s、2s、4s…并用Math.min(…, 10000)封顶为 10 秒避免退避时间失控该模式可与fetchWithTimeout、AbortController组合构成生产级重试基础设施。Promise 组合子速览与实战// Promise.allSettled - wait for all, regardless of rejection const results await Promise.allSettled([ fetch(/api/users), fetch(/api/posts), fetch(/api/invalid) ]); results.forEach((result, index) { if (result.status fulfilled) { console.log(Success ${index}:, result.value); } else { console.error(Failed ${index}:, result.reason); } }); // Promise.any - first successful result const fastestMirror await Promise.any([ fetch(https://mirror1.example.com/data), fetch(https://mirror2.example.com/data), fetch(https://mirror3.example.com/data) ]); // Promise.race - first settled (resolved or rejected) const raceResult await Promise.race([ fetchFromCache(), fetchFromNetwork() ]);三个组合子的适用场景差异显著Promise.allSettled()等待全部完成不因单个失败而中断每个结果用statusfulfilled/rejected区分。适合批量任务、逐项汇报进度的场景Promise.any()第一个成功的结果胜出。上面的镜像站示例非常适合 CDN/多节点容灾——任一镜像可用即返回Promise.race()第一个落定无论成功失败的结果胜出常见于缓存优先、失败再走网络或超时控制。异步生成器分页与流式拉取// Async generator for pagination async function* fetchPaginatedData(baseUrl) { let page 1; let hasMore true; while (hasMore) { const response await fetch(${baseUrl}?page${page}); const data await response.json(); yield data.items; hasMore data.hasMore; page; } } // Usage for await (const items of fetchPaginatedData(/api/items)) { processItems(items); }async function*声明异步生成器yield逐批产出数据调用方用for await...of消费分页状态page、hasMore被封装在生成器内部调用方只需处理items天然支持惰性求值——消费多少页才请求多少页可随时break中断不会多拉数据。带重试的流式消费// Async generator with error handling async function* streamWithRetry(source) { let retries 3; while (retries 0) { try { for await (const chunk of source) { yield chunk; } break; } catch (error) { retries--; if (retries 0) throw error; await delay(1000); } } }对流中段抛出的错误最多重试 3 次每次失败间隔 1 秒重试耗尽后向上抛出。注意break只在完整消费后触发防止已 yield 出的数据被重复产出。并发队列管理限制并发上限// Limit concurrent operations class AsyncQueue { #queue []; #running 0; #maxConcurrent; constructor(maxConcurrent 3) { this.#maxConcurrent maxConcurrent; } async run(fn) { while (this.#running this.#maxConcurrent) { await new Promise(resolve this.#queue.push(resolve)); } this.#running; try { return await fn(); } finally { this.#running--; const resolve this.#queue.shift(); if (resolve) resolve(); } } } // Usage const queue new AsyncQueue(2); const results await Promise.all( urls.map(url queue.run(() fetch(url))) );这是一个经典的**信号量Semaphore**实现含三个精妙细节私有字段#queue、#running、#maxConcurrent是 ES2022 语法与javascript-pro技能的 ES2023 约束一致见 modern-syntax.md自旋等待当运行中任务达到上限时run内部把resolve函数压入队列并await——任务完成时finally中弹出队头的resolve()恰好唤醒最早等待的调用方实现公平的 FIFO 调度计数修正#running--放在finally中即使fn()抛错也能正确释放槽位并唤醒下一个任务不会死锁。上面的用法把 100 个 URL 的下载限制为同时最多 2 个避免瞬时连接风暴压垮服务端。并发上限应根据下游承载能力数据库连接池大小、API 限流阈值确定。理解事件循环微任务与宏任务// Microtasks vs Macrotasks console.log(1: Synchronous); setTimeout(() console.log(2: Macrotask (setTimeout)), 0); Promise.resolve().then(() console.log(3: Microtask (Promise))); queueMicrotask(() console.log(4: Microtask (queueMicrotask))); console.log(5: Synchronous); // Output order: 1, 5, 3, 4, 2理解输出顺序1 → 5 → 3 → 4 → 2是理解整个事件循环的关键同步代码按书写顺序执行1、5微任务队列Promise 回调、queueMicrotask在同步代码结束后、下一轮宏任务之前批量清空故3、4紧接着执行注册顺序即执行顺序宏任务setTimeout、I/O在每轮事件循环末尾执行故2最后输出。这意味着任何Promise.resolve().then(...)或await的后续代码都会插队到宏任务之前。浏览器在渲染新帧前也会先清空微任务因此过度递归的 Promise 链可能导致页面长时间不渲染。避免阻塞事件循环分块让出// Avoid blocking the event loop const processLargeArray async (items) { const results []; const chunkSize 100; for (let i 0; i items.length; i chunkSize) { const chunk items.slice(i, i chunkSize); results.push(...chunk.map(processItem)); // Yield to event loop await new Promise(resolve setTimeout(resolve, 0)); } return results; };对大数组如数万条记录逐块处理每处理 100 项就await一个宏任务让出控制权使 UI 渲染、网络事件等宏任务能够插入执行避免卡死主线程。chunkSize可依据单条处理耗时调整。仓库中 assets/capture-screenshot.js 也体现了让出事件循环的思路——在page.goto完成后await new Promise((resolve) setTimeout(resolve, 2000))等待字体加载随后才截图// Wait for fonts to load await new Promise((resolve) setTimeout(resolve, 2000)); await page.screenshot({ path: path.join(__dirname, social-preview.png), ... });AbortController取消异步操作取消单个请求// Abort fetch requests const controller new AbortController(); const { signal } controller; setTimeout(() controller.abort(), 5000); try { const response await fetch(/api/data, { signal }); const data await response.json(); } catch (error) { if (error.name AbortError) { console.log(Request aborted); } }AbortController是 Fetch API 标准的取消机制通过signal与fetch建立关联controller.abort()会中断进行中的请求fetch抛出的错误name AbortError据此与网络错误、HTTP 错误区分上例 5 秒超时后自动中止可作为Promise.race超时方案的替代区别AbortController会真正终止底层网络请求释放连接。一次取消多个请求// Abort multiple operations const multiAbort async () { const controller new AbortController(); try { const [users, posts] await Promise.all([ fetch(/api/users, { signal: controller.signal }), fetch(/api/posts, { signal: controller.signal }) ]); } catch (error) { controller.abort(); throw error; } };同一controller.signal可以挂到多个fetch上只要其中任一请求失败导致Promise.allreject就立即abort()其余请求避免无谓的流量与连接占用。这个模式是fail-fast 且清理彻底的典范。流式处理ReadableStream 与 TransformStream读取响应流// Process ReadableStream const processStream async (url) { const response await fetch(url); const reader response.body.getReader(); const decoder new TextDecoder(); let result ; while (true) { const { done, value } await reader.read(); if (done) break; result decoder.decode(value, { stream: true }); } return result; };response.body是ReadableStreamreader.read()逐块读取二进制Uint8ArrayTextDecoder.decode(value, { stream: true })正确处理跨块的 UTF-8 多字节字符如中文、emoji避免块边界截断乱码适合大文件、SSEServer-Sent Events、日志流等无需整载入内存的场景。变换流// Transform streams const transformStream new TransformStream({ transform(chunk, controller) { const transformed chunk.toString().toUpperCase(); controller.enqueue(transformed); } }); const response await fetch(/data); const transformed response.body.pipeThrough(transformStream);TransformStream的transform钩子对流经的每个 chunk 做转换此处为大写化pipeThrough将读取流接入变换流。这一模式是流式管道pipe思想的基石可组合多个 TransformStream 实现过滤、限速、压缩等链式处理同时保持内存占用恒定。快速参考模式选型表PatternUse CaseExamplePromise.all()Parallel, fail-fastawait Promise.all([p1, p2])Promise.allSettled()Parallel, all resultsawait Promise.allSettled([p1, p2])Promise.race()First to completeawait Promise.race([p1, p2])Promise.any()First to succeedawait Promise.any([p1, p2])async function*Async iterationfor await (const x of gen())AbortControllerCancellationfetch(url, { signal })queueMicrotask()Priority microtaskqueueMicrotask(fn)与 javascript-pro 技能的协同使用async-patterns.md是javascript-pro技能按需加载的五份参考文档之一其余为 modern-syntax.md、modules.md、browser-apis.md、node-essentials.md见 SKILL.md 的 Reference Guide 表格。在实际开发中它们按上下文联动处理 Promise/async/await/事件循环问题 → 加载async-patterns.md本文涉及可选链、私有字段、ES2023 数组方法 → 加载modern-syntax.md浏览器 Fetch、Web Workers、Storage → 加载browser-apis.mdNode.js 的fs/promises、流、EventEmitter → 加载node-essentials.md。同时技能 SKILL.md 的约束应作为编写异步代码的底线禁止回调式模式一律使用 Promise、禁止var、禁止同步 I/O、不得忽略异步函数中的错误处理、不得造成内存泄漏或阻塞操作。将本文的模式与这些约束结合即可产出符合项目规范linter、Jest 85% 覆盖率、无未处理的 Promise rejection的异步代码。【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表