JavaScript异步编程 Async/Await 使用从原理到最佳实践记录
作者:Micro麦可乐
1.背景与概念
在传统 JavaScript
开发中,开发者长期面临回调地狱的困扰。随着 ES6 Promise
的出现,异步代码的可读性得到改善,但链式调用依然存在嵌套问题。2017 年 ES8 正式引入的 Async/Await
语法,让异步代码第一次拥有了同步代码般的可读性。
async
函数是基于 Promise
的语法糖,用于简化异步操作的书写方式。使用 async
声明的函数会隐式返回一个 Promise
,函数体内部可以通过 await
暂停执行,直到对应的 Promise
完成或抛出错误后再继续执行。await
操作符只能在 async
函数或模块顶层中使用,用于等待一个 Promise
解决,并将其结果作为表达式的值返回;如果 Promise
被拒绝,则会在该位置抛出异常,可配合常规的 try...catch
进行捕获处理。
这种写法极大地提升了异步代码的可读性,使得我们可以像编写同步代码一样直观地处理异步逻辑,同时保留了后台并发执行的优势。
2. 语法详解
2.1 声明与返回值
async function foo() { return 42; }
上例中, foo()
会返回一个已解决(fulfilled)的 Promise
,其值为 42;等价于:
function foo() { return Promise.resolve(42); }
这是因为任何 async
函数内的返回值都会被自动封装为 Promise
2.2 使用 await 暂停执行
async function fetchData() { let response = await fetch('/api/data'); let data = await response.json(); return data; }
代码解释:
- 第一行的
await fetch(...)
会暂停fetchData
的执行,直到fetch
返回的Promise
完成,并将其结果赋值给 response - 第二行的
await response.json()
同理,等待解析JSON
后再继续执行 - 如果任一
Promise
拒绝,则会在该await
位置抛出异常,可在外层使用try...catch
捕捉。
2.3 错误处理
async function safeFetch() { try { let res = await fetch('/bad/url'); let json = await res.json(); return json; } catch (err) { console.error('请求失败:', err); throw err; // 可再次抛出或返回默认值 } }
上述模式与同步代码中使用 try…catch 完全一致,大大简化了基于 Promise 链式 .catch() 的写法
2.4 语法规则
下面我们看看我们常用的一些使用语法
// 声明异步函数 async function fetchUser() { return { name: 'Alice', age: 28 }; // 自动包装为Promise } // 使用箭头函数 const fetchData = async () => { const res = await fetch('/api/data'); return res.json(); }; // 立即调用模式 (async () => { const data = await fetchData(); console.log(data); })();
3. 并发与性能
3.1 顺序等待 vs 并行等待
默认情况下,连续的 await 会串行执行:
let a = await task1(); let b = await task2();
若两者互不依赖,可改为并行:
let [a, b] = await Promise.all([task1(), task2()]);
3.2 并行执行优化
通过上面 顺序等待 vs 并行等待 的介绍,通常我们可以按照以下形式来进行优化(模拟请求)
// 顺序执行(总耗时 = 各请求耗时之和) async function serialRequests() { const res1 = await fetch('/api/1'); const res2 = await fetch('/api/2'); return [await res1.json(), await res2.json()]; } // 并行执行(总耗时 ≈ 最慢请求耗时) async function parallelRequests() { const [res1, res2] = await Promise.all([ fetch('/api/1'), fetch('/api/2') ]); return await Promise.all([res1.json(), res2.json()]); }
3.2 限制并发数量
在需要对大量异步任务进行限流时,可使用第三方库(如 p-limit
)或自己实现简单队列,避免一次性发起过多请求导致资源竞争或网络拥堵
4. 异步迭代
ES2018
引入了 for await...of
,用于遍历异步可迭代对象(如异步生成器):
async function* gen() { yield await fetchChunk(1); yield await fetchChunk(2); } (async () => { for await (let chunk of gen()) { console.log(chunk); } })();
该语法在处理流式数据(例如文件分块下载)时非常有用
5 常见问题解决方案
5.1 请求重试机制
在外面日常开发中,会遇到请求失败需要重试的需求,来看看以下模拟代码
async function fetchWithRetry(url, retries = 3) { for (let i = 0; i < retries; i++) { try { const res = await fetch(url); return await res.json(); } catch (err) { if (i === retries - 1) throw err; await new Promise(r => setTimeout(r, 1000 * (i + 1))); } } }
5.2 竞态条件处理
当多个请求并发执行时,可能因网络延迟、服务器响应速度差异等问题导致响应顺序与发送顺序不一致问题。
更详细讲解可以查阅博主写过一篇【 前端请求乱序问题分析与AbortController、async/await、Promise.all等解决方案】
以下仅展现实现代码:
let lastController = null; async function search(query) { // 取消前一个未完成的请求 if (lastController) lastController.abort(); const controller = new AbortController(); lastController = controller; try { const res = await fetch(`/api/search?q=${query}`, { signal: controller.signal }); return await res.json(); } catch (err) { if (err.name !== 'AbortError') throw err; } }
5.3 异步生成器
在 ·async function*· 中,你既可以使用 ·await·,也可以使用 ·yield·,将异步任务与懒加载结合:
async function* asyncGenerator() { for (let i = 0; i < 3; i++) { await delay(1000); yield i; } }
这种方式适合按需获取异步数据,提高资源利用率
5.4 处理非 Promise 值
await
后可以跟任意表达式,如果其值不是 Promise
,则会被包装为立即解决的 Promise
。例如:
let x = await 123; // 相当于 await Promise.resolve(123)
但建议对非异步操作避免使用 await,以免误导
5.5 常见陷阱
- 遗忘 await:调用 async 函数但未加 await,会得到未决(pending)的 Promise 而非预期结果
- 在非 async 环境使用 await:仅在模块顶层或 async 函数内部可用,否则会抛语法错误
- Promise.all 中单个失败导致整体失败:若需要容忍部分失败,可对内部 Promise 使用 .catch() 处理,避免整体拒绝
- 滥用并发:同时发起过多网络请求可能触发限流或阻塞,建议根据场景调整并发策略
6. 性能优化实践
博主这里例举两个优化的案例:内存管理以及优先加载优化
6.1 内存管理
常见一些大量数据的获取下载
async function processLargeData() { const data = await getHugeData(); // 大数据量 // 分块处理 for (let i = 0; i < data.length; i += 1000) { const chunk = data.slice(i, i + 1000); await processChunk(chunk); data[i] = null; // 及时释放内存 } }
6.2 优先加载优化
async function loadCriticalResources() { // 预加载非关键资源 const nonCritical = fetch('/non-critical').then(r => r.json()); // 优先处理关键资源 const user = await fetchUser(); const config = await fetchConfig(); // 等待非关键资源 const data = await nonCritical; return { user, config, data }; }
7. 结语
Async/Await
的引入彻底改变了 JavaScript
异步编程的面貌。通过本文的讲解,相信小伙伴们可以掌握 Async/Await
的使用精髓,将使您的 JavaScript
代码在保持高性能的同时,获得质的可读性和可维护性提升。
到此这篇关于JavaScript异步编程 Async/Await 使用详解:从原理到最佳实践的文章就介绍到这了,更多相关js异步编程Async/Await 使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!