async/await 这东西,说难不难,说简单也不简单。
很多人用了很久,但真要解释"它底层怎么跑的",就开始含糊了。这篇文章就是要把这个说清楚。
先从一个问题开始
你有没有想过,这段代码为什么能"暂停"?
async function fetchUser() {
const res = await fetch('/api/user');
const data = await res.json();
return data;
}
JavaScript 是单线程的,理论上不能"等"——一旦卡住,整个页面就冻结了。但 await 明明就在等,而且等的时候页面还能正常响应。
这是怎么做到的?
先搞懂 Promise
async/await 是 Promise 的语法糖,所以得先知道 Promise 是什么。
Promise 本质上就是一个状态机,三种状态:
- pending:等待中
- fulfilled:成功了
- rejected:失败了
状态只能从 pending 变成另外两种,而且不可逆。
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000);
});
p.then(result => console.log(result)); // 1秒后打印 "done"
关键点:.then() 里的回调不是立刻执行的,它被放进了微任务队列,等当前同步代码跑完再执行。
事件循环:JavaScript 的调度核心
要理解 async/await,必须知道事件循环(Event Loop)。
JavaScript 的执行模型大概是这样:
调用栈(Call Stack)
↓ 同步代码在这里执行
微任务队列(Microtask Queue)
↓ Promise.then、queueMicrotask 等
宏任务队列(Macrotask Queue)
↓ setTimeout、setInterval、I/O 等
执行顺序:
- 跑完调用栈里的同步代码
- 清空微任务队列(全部跑完)
- 取一个宏任务执行
- 再清空微任务队列
- 循环往复
这就是为什么 Promise 的回调比 setTimeout 先执行:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序:1 → 4 → 3 → 2
async/await 的真面目
async 函数本质上是一个返回 Promise 的函数。
async function foo() {
return 42;
}
// 等价于
function foo() {
return Promise.resolve(42);
}
await 则是暂停当前 async 函数的执行,等 Promise 完成后再继续。
但"暂停"不是真的停住——它只是把后面的代码包成一个回调,注册到 Promise 的 .then() 里,然后把控制权还给调用者。
用伪代码理解:
async function fetchUser() {
const res = await fetch('/api/user');
const data = await res.json();
return data;
}
// 大概等价于
function fetchUser() {
return fetch('/api/user').then(res => {
return res.json().then(data => {
return data;
});
});
}
所以 await 并没有阻塞线程,它只是把"等待之后的逻辑"推迟到 Promise 完成时执行。
生成器:async/await 的前身
async/await 的实现原理和生成器(Generator)密切相关。
生成器可以在函数执行中途暂停,然后从暂停的地方继续:
function* gen() {
console.log('step 1');
yield;
console.log('step 2');
yield;
console.log('step 3');
}
const g = gen();
g.next(); // 打印 "step 1",暂停
g.next(); // 打印 "step 2",暂停
g.next(); // 打印 "step 3",结束
把生成器和 Promise 结合起来,就能实现"等 Promise 完成后继续执行"的效果。这正是 async/await 在语言层面做的事。
早期没有 async/await 时,社区用 co 这个库来实现类似效果:
// co 库的用法(历史产物,了解即可)
co(function* () {
const res = yield fetch('/api/user');
const data = yield res.json();
return data;
});
async/await 就是把这个模式内置到语言里了。
一个完整的执行过程
来看这段代码,逐步分析执行顺序:
async function main() {
console.log('A');
const result = await Promise.resolve('hello');
console.log('B', result);
}
console.log('start');
main();
console.log('end');
执行过程:
- 打印
start - 调用
main(),打印A - 遇到
await,把console.log('B', result)注册为微任务,main 函数暂停,控制权返回 - 打印
end - 同步代码跑完,清空微任务队列
- 打印
B hello
输出:start → A → end → B hello
很多人会以为是 start → A → B hello → end,这是个常见误区。
错误处理
async/await 的错误处理比 Promise 链直观很多:
// Promise 链写法
fetch('/api/user')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
// async/await 写法
async function fetchUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
try/catch 能捕获 await 抛出的错误,包括网络错误、JSON 解析错误等。
有一个细节要注意:如果 async 函数里没有 try/catch,错误会变成 rejected 的 Promise,需要在调用处处理:
async function fetchUser() {
const res = await fetch('/api/user'); // 如果失败,会抛出
return await res.json();
}
// 调用处处理
fetchUser().catch(err => console.error(err));
// 或者
try {
await fetchUser();
} catch (err) {
console.error(err);
}
并发执行
await 是串行的,一个等完再等下一个。如果两个请求互不依赖,串行就浪费时间了:
// 串行:总耗时 = 请求1时间 + 请求2时间
async function serial() {
const user = await fetchUser(); // 等 500ms
const posts = await fetchPosts(); // 再等 300ms
// 总共 800ms
}
// 并发:总耗时 = max(请求1时间, 请求2时间)
async function parallel() {
const [user, posts] = await Promise.all([
fetchUser(), // 同时发出
fetchPosts() // 同时发出
]);
// 总共 500ms
}
Promise.all 同时发起多个请求,等全部完成后返回结果数组。
手搓 async/await:从零实现一遍
光看原理不过瘾,自己实现一遍才真的懂。
第一步:手写一个 Promise
先把 Promise 的核心逻辑实现出来:
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.callbacks = []; // 存放 then 注册的回调
const resolve = (value) => {
if (this.state !== 'pending') return;
this.state = 'fulfilled';
this.value = value;
// 通知所有等待的回调
this.callbacks.forEach(cb => cb.onFulfilled(value));
};
const reject = (reason) => {
if (this.state !== 'pending') return;
this.state = 'rejected';
this.value = reason;
this.callbacks.forEach(cb => cb.onRejected(reason));
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
// 返回新的 Promise,支持链式调用
return new MyPromise((resolve, reject) => {
const handle = (fn, val) => {
// 用 queueMicrotask 模拟微任务
queueMicrotask(() => {
try {
const result = fn(val);
// 如果返回的也是 Promise,等它完成
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (err) {
reject(err);
}
});
};
if (this.state === 'fulfilled') {
handle(onFulfilled, this.value);
} else if (this.state === 'rejected') {
handle(onRejected, this.value);
} else {
// 还在 pending,先存起来等 resolve/reject 触发
this.callbacks.push({
onFulfilled: val => handle(onFulfilled, val),
onRejected: val => handle(onRejected, val),
});
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
return new MyPromise(resolve => resolve(value));
}
static reject(reason) {
return new MyPromise((_, reject) => reject(reason));
}
}
验证一下:
new MyPromise((resolve) => {
setTimeout(() => resolve('hello'), 500);
}).then(val => {
console.log(val); // 500ms 后打印 "hello"
return val + ' world';
}).then(val => {
console.log(val); // 打印 "hello world"
});
第二步:用生成器模拟 await
生成器的 yield 可以暂停函数,这和 await 的行为一模一样。
先写一个"执行器",让生成器自动跑完:
function runGenerator(genFn) {
return new MyPromise((resolve, reject) => {
const gen = genFn(); // 拿到生成器对象
function step(nextFn) {
let result;
try {
result = nextFn(); // 执行到下一个 yield
} catch (err) {
return reject(err);
}
if (result.done) {
// 生成器跑完了,resolve 最终值
return resolve(result.value);
}
// result.value 是 yield 右边的 Promise
// 等它完成后,把结果传回生成器继续执行
MyPromise.resolve(result.value).then(
val => step(() => gen.next(val)), // 成功:继续
err => step(() => gen.throw(err)) // 失败:抛错
);
}
step(() => gen.next()); // 启动
});
}
用起来是这样的:
// 模拟一个异步请求
function fakeRequest(url) {
return new MyPromise(resolve => {
setTimeout(() => resolve(`data from ${url}`), 300);
});
}
// 用生成器写"同步风格"的异步代码
runGenerator(function* () {
console.log('开始请求');
const user = yield fakeRequest('/api/user');
console.log('用户数据:', user);
const posts = yield fakeRequest('/api/posts');
console.log('文章数据:', posts);
return '全部完成';
}).then(result => {
console.log(result);
});
// 输出:
// 开始请求
// 用户数据: data from /api/user
// 文章数据: data from /api/posts
// 全部完成
这就是 co 库的核心逻辑,也是 async/await 的底层原理。
第三步:封装成 async/await 的形式
把上面的 runGenerator 包一层,就得到了 async 函数的效果:
function myAsync(genFn) {
return function(...args) {
return runGenerator(function* () {
return yield* genFn(...args); // 代理生成器
});
};
}
用法:
const fetchUser = myAsync(function* () {
const res = yield fakeRequest('/api/user');
const data = yield fakeRequest('/api/parse');
return data;
});
// 和真正的 async 函数用法一样
fetchUser().then(data => console.log(data));
第四步:加上错误处理
真实的 async/await 支持 try/catch,生成器也支持:
runGenerator(function* () {
try {
const data = yield MyPromise.reject(new Error('请求失败'));
console.log(data); // 不会执行
} catch (err) {
console.log('捕获到错误:', err.message); // 打印 "捕获到错误: 请求失败"
}
});
当 Promise reject 时,执行器调用 gen.throw(err),把错误抛进生成器,生成器里的 try/catch 就能捕获到。
完整实现汇总
// 1. 简版 Promise
class MyPromise { /* 见上文 */ }
// 2. 生成器执行器(async/await 的核心)
function runGenerator(genFn) {
return new MyPromise((resolve, reject) => {
const gen = genFn();
function step(nextFn) {
let result;
try { result = nextFn(); } catch (err) { return reject(err); }
if (result.done) return resolve(result.value);
MyPromise.resolve(result.value).then(
val => step(() => gen.next(val)),
err => step(() => gen.throw(err))
);
}
step(() => gen.next());
});
}
// 3. async 函数工厂
function myAsync(genFn) {
return function(...args) {
return runGenerator(() => genFn(...args));
};
}
// 4. 使用示例
const main = myAsync(function* () {
try {
const user = yield fakeRequest('/api/user');
const posts = yield fakeRequest('/api/posts');
return { user, posts };
} catch (err) {
console.error('出错了:', err);
}
});
main().then(result => console.log('结果:', result));
跑一遍这段代码,你就真的理解 async/await 了。
总结
async/await 的工作原理,核心就三点:
- async 函数返回 Promise,函数内部的 return 值会被包成 resolved 的 Promise
- await 暂停函数执行,把后续代码注册为微任务,把控制权还给调用者,不阻塞线程
- 底层依赖事件循环,微任务队列保证了 await 之后的代码在当前同步任务完成后立即执行
手搓一遍的收获:
- Promise 的链式调用靠的是每次
.then()返回新 Promise - 生成器的
yield就是await的前身 - 执行器负责"驱动"生成器一步步跑完
- 错误通过
gen.throw()注入生成器,被 try/catch 捕获
理解了这些,async/await 的各种"奇怪行为"就都能解释了。