《中级前端进阶方向 第一步)JavaScript 微任务 / 宏任务机制》
《中级前端进阶方向 第二步)深入事件循环(Event Loop)》
《中级前端进阶方向 第三步)深入javascript原型链,附学习代码》
《中级前端进阶方向 第四步)深入javascript 闭包、this 、作用域提升,附学习案例源码》
《中级前端进阶方向 第五步)深入 javascript高级特性Proxy,附学习案例源码》
《中级前端进阶方向 第六步)深入 javascript 高级特性Generator生成器,附案例源码》
--🖍《延伸扩展》-----------------------------------------------------------------------------------------
《中级前端进阶方向 延伸扩展一)javascript 私有状态/工厂函数/回调中保持局部状态/防抖与节流/IIFE 捕获》
1) 概念速览
-
async
/await
是基于Promise
的语法糖,用来把基于回调/then 的异步代码写得像同步代码一样,增强可读性和可维护性。 -
async
标记一个函数为异步函数,该函数总是返回一个Promise
(即使返回值是普通值也会被Promise.resolve
包裹)。 -
await
用在async
函数内部,等待一个Promise
解析(或等待 thenable),并把解析值返回。await
会“暂停”当前 async 函数的执行,但不会阻塞整个 JS 运行时 / 事件循环。
2) 基本语法:
async function foo() {
return 42; // 等同于 return Promise.resolve(42)
}
foo().then(v => console.log(v)); // 42
async function bar() {
const a = await Promise.resolve(1);
const b = await 2; // await 一个非 Promise 值 -> 直接得到该值
return a + b; // 返回值同样被包成 Promise
}
bar().then(console.log); // 3
关键特性:一个 async
函数 永远返回一个 Promise
-
如果函数内部返回一个非 Promise 的值(如字符串、数字、对象),
async
函数会自动用Promise.resolve()
将其包装成一个已解决的 Promise(fulfilled Promise)。 -
如果函数内部显式返回一个 Promise,那么就以这个 Promise 为准。
-
如果函数内部抛出异常,它会返回一个被拒绝的 Promise(rejected Promise)。
3) await
的微任务(task)细节
await
本质上是把后面的继续执行放到微任务队列(microtask)中。因此:
console.log('start');
(async () => {
console.log('inside before await');
await null; // 将后续部分排为微任务
console.log('inside after await');
})();
console.log('end');
输出顺序:
start
-> inside before await
-> end
-> inside after await
说明:调用 async 函数时,函数体会同步执行直到遇到第一个 await
(或执行完)。await
后的代码被排入微任务队列,当前宏任务结束后执行微任务(任务队列可以看第二步)
await
的工作流程:
-
await
后面的表达式通常是一个 Promise,但也可以是一个任何值 -
如果表达式是非 Promise 的值,
await
会直接返回这个值 -
如果表达式是 Promise,
await
会“阻塞”后面的代码,等待 Promise 完成。-
如果 Promise 成功解决(fulfilled),
await
会返回 Promise 的解决值(resolution value)。 -
如果 Promise 被拒绝(rejected),
await
会抛出拒绝的原因(rejection reason),就像一个throw
语句一样。
-
注意:await
关键字只能在 async
函数内部使用。它的作用是暂停异步函数的执行,等待一个 Promise 对象的解决(settle),然后恢复函数的执行并返回结果。
4) 串行与并行(非常常见的性能问题)
await
在循环中会导致串行执行(若你逐项 await
):串行(慢):
for (const url of urls) {
const resp = await fetch(url);
const data = await resp.json();
results.push(data);
}
并行(快)——推荐:先启动所有 Promise,再 await
:
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);
注意:
-
Promise.all
一旦有一个失败,会导致整体 reject(可用allSettled
处理每项结果)。 -
如果要“并发但限制并发数”,需要并发控制(见下)。
5) 常见坑与反模式
-
array.forEach(async item => { await doAsync(item); })
—— 这不会等待内部 async 完成;forEach
不能与await
配合控制流。改用for..of
或Promise.all
。 -
忘记
await
:const result = fetch(url).then(...);
vsconst result = await fetch(url);
-
在顶层用
await
:只有在 ESM 模块或支持顶层 await 的环境中才可用(现代浏览器、Node 的 ESM 模式支持)。 -
await
并不创建新线程,不能用于替代 CPU 密集型任务的分片(CPU 密集应交由 worker / 子进程)。
示例错误用法:
[1,2,3].forEach(async n => {
await doSomething(n); // forEach 不等待这些 async,外部无法 await 完成
});
正确做法:
for (const n of [1,2,3]) {
await doSomething(n); // 串行
}
// 或并发
await Promise.all([1,2,3].map(n => doSomething(n)));
6) 错误处理(try / catch / finally)
推荐在 async
中用 try/catch
,便于捕获 await
抛出的错误:
async function getData() {
try {
const resp = await fetch('/api/data');
if (!resp.ok) throw new Error('bad status');
return await resp.json();
} catch (err) {
console.error('请求失败:', err);
throw err; // 可选:重新抛出使调用方知道
} finally {
// 清理工作(可选)
}
}
对于并发多个任务而要逐项处理错误,Promise.allSettled
很有用:
const results = await Promise.allSettled(promises);
results.forEach(r => {
if (r.status === 'fulfilled') { /* r.value */ }
else { /* r.reason */ }
});
7) 取消与超时(AbortController 与超时模式)
浏览器 fetch 与现代 Node.js 支持 AbortController
,可用于取消请求:
async function fetchWithTimeout(url, ms) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), ms);
try {
const resp = await fetch(url, { signal: controller.signal });
return await resp.json();
} finally {
clearTimeout(id);
}
}
另一种经典超时模式(不优雅,旧法):
const response = await Promise.race([
fetch(url),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000))
]);
推荐使用 AbortController
,因为可以在超时时真正取消底层请求
8) 并发限制(实现一个简单的 p-limit)
当你需要对大量任务并发做限流时,可以用简单的队列实现:
function pLimit(concurrency) {
const queue = [];
let active = 0;
const next = () => {
if (active >= concurrency || queue.length === 0) return;
active++;
const { fn, resolve, reject } = queue.shift();
(async () => {
try {
const val = await fn();
resolve(val);
} catch (err) {
reject(err);
} finally {
active--;
next();
}
})();
};
return (fn) => new Promise((resolve, reject) => {
queue.push({ fn, resolve, reject });
next();
});
}
// 用法:
const limit = pLimit(3); // 最多 3 个并发
const tasks = urls.map(url => limit(() => fetch(url).then(r => r.json())));
const results = await Promise.all(tasks);
这个模式很实用
9) 设计模式与实用技巧
-
重试(带指数退避):
async function retry(fn, attempts = 3) {
let i = 0;
while (i < attempts) {
try {
return await fn();
} catch (err) {
i++;
if (i >= attempts) throw err;
await new Promise(r => setTimeout(r, 2 ** i * 100)); // 指数退避
}
}
}
-
后台(fire-and-forget)并安全记录错误:
(async () => {
try {
await doSomething();
} catch (err) {
console.error('后台任务失败', err);
}
})(); // 立即执行但错误被捕获
-
在顶层用 await(仅在模块/支持环境):
// 在 ESM 模块中
const data = await fetch('/api').then(r => r.json());
-
将同步/异步分开:不要在 sync-heavy 函数里混用大量 await,考虑把耗时 I/O 单独封装。
10) async 函数的同步部分与任务调度(关键理解点)
调用 async
函数会 立即 执行函数体直到第一个 await
(或结束),这些同步代码不会被延迟。示例:
async function f() {
console.log('start f');
await 1;
console.log('after await');
}
console.log('before call');
f();
console.log('after call');
// 输出: before call -> start f -> after call -> after await
这是理解控制流顺序与调试非常重要的点。
11) 错误处理:使用 try...catch
await
在遇到被拒绝的 Promise 时会抛出异常,我们可以使用经典的 try...catch
块来捕获错误,这使得错误处理变得同步化,非常直观。
没有 Async/Await (Promise 风格):
function fetchUser() {
return fetch('/api/user')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(user => console.log(user))
.catch(error => console.error('Fetch failed:', error)); // 统一捕获错误
}
使用 Async/Await:
async function fetchUser() {
try {
const response = await fetch('/api/user');
if (!response.ok) {
// throw 的错误会被后面的 catch 捕获
throw new Error('Network response was not ok');
}
const user = await response.json(); // 等待第二个异步操作
console.log(user);
} catch (error) {
// 捕获所有 try 块内 await 抛出的错误,以及显式 throw 的错误
console.error('Fetch failed:', error);
}
}
你也可以在调用 async
函数后使用 .catch()
,因为它在任何情况下都返回 Promise。
fetchUser().catch(error => console.error('Fetch failed externally:', error));
12) 与 Promise 链的对比
Promise 链式调用:
function getuserAndPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(userResponse => userResponse.json())
.then(user => {
console.log('User:', user);
return fetch(`/api/posts?userId=${user.id}`);
})
.then(postsResponse => postsResponse.json())
.then(posts => {
console.log('Posts:', posts);
return posts; // 最终返回值
})
.catch(error => {
console.error('Error:', error);
});
}
Async/Await:
async function getuserAndPosts(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
console.log('User:', user);
const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsResponse.json();
console.log('Posts:', posts);
return posts; // 最终返回值
} catch (error) {
console.error('Error:', error);
}
}
优势一目了然:
-
代码结构:Async/Await 的代码是从上到下线性执行的,非常符合人类的阅读习惯。它消除了
.then()
的嵌套,代码更扁平。 -
变量作用域:在
try
块中,所有中间变量(如userResponse
,user
)都在同一个作用域内,无需像 Promise 链那样通过传递参数或在外部定义变量。 -
条件逻辑和循环:处理
if/else
和循环(如for
,while
)中的异步操作变得异常简单,而这在纯 Promise 链中是非常棘手的。
13) 进阶用法和注意事项
并行执行:使用 Promise.all
上面的例子是串行执行,第二个请求必须等第一个请求完成才能开始。如果多个异步操作之间没有依赖关系,并行执行可以大幅节省时间。
async function fetchIndependentData() {
// 错误:这样写是串行的,会等待第一个 await 完成再执行第二个
const a = await fetch('/api/a');
const b = await fetch('/api/b');
// 正确:使用 Promise.all 实现并行
const [resultA, resultB] = await Promise.all([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json())
]);
console.log(resultA, resultB);
return { resultA, resultB };
}
Promise.all
接受一个 Promise 数组,并返回一个新的 Promise。这个新的 Promise 会在所有输入的 Promise 都成功解决后才解决,其解决值是一个结果数组,顺序与输入数组一致。如果其中任何一个被拒绝,整个 Promise.all
会立即被拒绝。
await
在循环中的使用
串行循环(一个接一个执行):
async function processArray(array) {
for (const item of array) {
await processItem(item); // 等待上一个处理完再处理下一个
}
}
并行循环(同时执行):
async function processArray(array) {
// 使用 map 创建所有 Promise,然后用 Promise.all 并行等待
const promises = array.map(item => processItem(item));
const results = await Promise.all(promises);
console.log(results);
}
顶层 Await (Top-Level Await)
在 ES2022 之前,await
不能直接在模块的顶层作用域使用,必须包裹在 async
函数中。现在,在 ES 模块(<script type="module">
) 中,可以直接使用顶层 await
。
// 在 ES Module 中
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
// 模块会等待顶层 await 解决后才执行导入此模块的代码
14) 常用组合一览(备忘小抄)
-
串行处理:
for (const item of items) {
await do(item);
}
-
并行处理(全部成功才继续):
await Promise.all(items.map(item => do(item)));
-
并行但收集个别错误:
const results = await Promise.allSettled(promises);
-
竞速(先完成者):
const r = await Promise.race([p1, p2]);
-
超时:
const r = await Promise.race([fetch(url), timeoutPromise(3000)]);
或使用 AbortController
更优雅地取消请求。
-
限制并发:使用上文
pLimit
或成熟库p-limit
。
15) 总结:
与旧式回调 / generator 的关系
-
async/await
是基于Promise
的更友好语法,generator + co
也曾被用来写同步风格异步(co
库),现在基本被async/await
取代。 -
await
更直观、更易读,且已被广泛原生支持。
TypeScript 相关注意
-
在 TypeScript 中,
async function foo(): Promise<T> {}
给出明确返回类型。 -
await
的结果会被类型系统推断:const x = await fetchJson()
,x
的类型是Promise
解包后的类型。 -
须注意
unknown
/any
的捕获处理与类型收窄在try/catch
中的差异(catch 的错误默认为unknown
,建议先类型判断)。
调试与性能提示
-
async
/await
会产生额外的 promise/microtask,但通常代价较小。避免在高频短函数里无谓地包一层async
(例如非常短的循环内每次都创建很多微任务会有一点 overhead)。 -
若要观察未捕获的 Promise rejection,Node 环境中使用
process.on('unhandledRejection', ...)
(或在浏览器里监听unhandledrejection
事件)。 -
若需要让界面“让出”控制权以便渲染,
await Promise.resolve()
只会将后续放到微任务,可能不足以触发渲染;可使用await new Promise(r => setTimeout(r, 0))
或平台特定 API。
特性 | 说明 |
---|---|
async | 声明一个异步函数,该函数总是返回一个 Promise。 |
await | 暂停 async 函数的执行,等待一个 Promise 的解决,并返回其结果。只能在 async 函数内使用。 |
返回值 | async 函数return的值会成为其返回的 Promise 的解决值。 |
错误处理 | 使用 try...catch 来捕获 await 表达式可能抛出的错误,与同步代码错误处理方式一致。 |
核心优势 | 代码更清晰、更易读,像写同步代码一样写异步代码。轻松处理条件分支和循环中的异步操作。 |
底层原理 | 它是基于 Promise 的语法糖,并没有取代 Promise,而是与之协同工作。 |
并行优化 | 对于无依赖的异步操作,应结合 Promise.all 使用以实现并行,提升性能。 |
-
尽量用
for..of
或Promise.all
,避免forEach
+await
的误用。 -
并发量大时加并发限制,防止资源耗尽(文件描述符、socket、内存)。
-
关键 I/O (请求/DB)应支持取消(AbortController)与超时。
-
使用
try/catch
明确处理错误;对于并行任务用allSettled
以便逐项处理失败。 -
避免把大量 CPU 密集型逻辑放在 async 中期望
await
帮忙“转移”——它只对 I/O 有帮助。 -
在库/公共 API 中显式返回
Promise
类型(TypeScript)以便调用者明确知道可await
。
总而言之,Async/Await 是现代 JavaScript 中处理异步操作的首选方式。它极大地改善了代码的可读性和可维护性,是每个 JS 开发者必须掌握的核心技能。