“异步”不异步?从 fetch 到 async/await,我拨开了 JavaScript 的面纱
本文带你深入掘金一线大厂面试官最爱问的异步编程底层逻辑——从 Promise 到 async/await,不止会用,更要懂它为什么能“等”。
引子:你以为你写的 async 是同步?
const main = async () => {
const res = await fetch('https://api.github.com/users/shunwuyu/repos');
console.log(res);
console.log(111);
const data = await res.json();
console.log(data);
}
main();
这段代码看起来像“同步”,但真的是同步吗?
如果你在面试中只说:“await 让异步变同步”,恭喜你——离挂掉不远了。
今天我们就以这段看似简单的代码为引子,层层拆解 async/await 背后的执行机制、事件循环调度、Promise 封装原理,并结合 Node.js 中的文件读取场景,打通浏览器与服务端异步模型的认知壁垒。
一、从回调地狱到 async/await:异步演进简史
1. 回调函数(Callback):信任危机的开始
fs.readFile('./1.html', 'utf-8', (err, data) => {
if (err) return console.error(err);
console.log(data);
});
问题:回调嵌套深、错误处理分散、控制流难以追踪。这就是著名的“回调地狱”。
2. Promise:给异步一个“契约”
const p = new Promise((resolve, reject) => {
fs.readFile('./1.html', 'utf-8', (err, data) => {
err ? reject(err) : resolve(data);
});
});
p.then(data => console.log(data));
Promise 的核心思想是:将异步操作的结果封装成一个对象,通过 .then() 链式消费。但它仍有痛点:
- 语法冗长
- 错误需每个
.catch()单独处理 - 无法直观表达“顺序依赖”
3. async/await:语法糖?还是革命?
const html = await p;
console.log(html);
表面是糖,底层是 Promise + Generator 的精妙组合。
ES2017(ES8)引入的 async/await,本质是 基于 Promise 的语法糖,但它极大提升了代码可读性与心智模型一致性。
✅ 关键认知:
async函数永远返回一个 Promise。即使你return 42,也会被包装成Promise.resolve(42)。
二、逐行解析:你的 await 到底干了什么?
回到开头的浏览器代码:
const res = await fetch('https://api.github.com/users/shunwuyu/repos');
Step 1: fetch 返回的是什么?
fetch() 返回一个 Promise。注意:此时网络请求已经发出,但响应尚未到达。
Step 2: await 做了什么?
- 暂停当前
async函数的执行(不是整个线程! ) - 将后续代码(
console.log(res)及之后)注册为该 Promise 的.then()回调 - 控制权交还给事件循环(Event Loop)
🧠 面试高频点:
await不会阻塞主线程!它只是让当前函数“暂停”,其他任务(如用户点击、定时器)仍可执行。
Step 3: res.json() 为何也要 await?
因为 res.json() 也返回一个 Promise!HTTP 响应体是流(stream),.json() 是异步解析过程。
// 错误写法 ❌
const data = res.json(); // data 是 Promise,不是对象!
// 正确写法 ✅
const data = await res.json();
💡 设计哲学:所有可能涉及 I/O 的操作都应是异步的,避免阻塞渲染或事件处理。
三、Node.js 场景:自定义 Promise 包装 fs
这段代码非常典型:
const p = new Promise((resolve, reject) => {
fs.readFile('./1.html', 'utf-8', (err, data) => {
err ? reject(err) : resolve(data);
});
});
为什么需要手动包装?
因为 fs.readFile 是 Node.js 的回调风格 API,不属于 Promise 规范。为了能用 await,必须将其“Promise 化”。
更优雅的写法:util.promisify
Node.js 内置工具帮你自动转换:
import { promisify } from 'util';
import { readFile } from 'fs';
const readFileAsync = promisify(readFile);
const html = await readFileAsync('./1.html', 'utf-8');
✅ 最佳实践:优先使用原生 Promise API 或
promisify,避免手动写 Promise 构造器(易出错,如忘记 reject)。
四、深入底层:async/await 与事件循环
很多人以为 await 是“等待”,其实它是 事件循环的调度指令。
考虑以下代码:
async function foo() {
console.log('A');
await Promise.resolve();
console.log('B');
}
foo();
console.log('C');
输出顺序:A → C → B
为什么?
await Promise.resolve()相当于Promise.resolve().then(() => console.log('B')).then()回调被放入 微任务队列(Microtask Queue)- 当前同步代码(
console.log('C'))执行完后,才清空微任务队列
🔥 面试必考:宏任务 vs 微任务。
setTimeout是宏任务,Promise.then/await是微任务。
五、延伸思考:性能与错误处理
1. 并行 vs 串行
// 串行(慢)
const user = await fetchUser();
const posts = await fetchPosts();
// 并行(快)
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
⚠️ 注意:
Promise.all任一 reject 会整体失败,可用Promise.allSettled替代。
2. 错误处理:try/catch 是你的朋友
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Network error');
const data = await res.json();
} catch (err) {
console.error('Fetch failed:', err);
}
✅ 统一错误入口,比
.catch()更符合直觉。
六、高频面试题关联
| 面试题 | 本文覆盖点 |
|---|---|
async/await 原理? | 基于 Promise + Generator,语法糖 |
await 会阻塞吗? | 不阻塞主线程,仅暂停当前函数 |
如何实现 sleep? | await new Promise(r => setTimeout(r, ms)) |
| 微任务有哪些? | Promise.then, await, queueMicrotask |
| 如何将回调转 Promise? | 手动包装 or util.promisify |
结语:别再把 async 当“同步”用了
async/await 的伟大之处,在于用同步的写法表达异步的逻辑,但它从未改变 JavaScript 单线程、非阻塞的本质。
真正的大厂工程师,不仅会写 await,更清楚:
- 它如何与事件循环协作
- 它在 V8 引擎中如何被编译
- 它与 Promise 的等价转换关系
- 它在性能优化中的取舍
下次面试,当被问“await 底层怎么实现的?”,你可以微微一笑:“要不,我们聊聊 Generator 和状态机?”