JavaScript(八股试题003):常见的三种异步实现方式详细讲解
一、核心讲解
背景知识
JavaScript 是 单线程事件循环机制,所以必须支持异步编程才能提高运行效率。
异步编程的语法目标:让异步过程能像同步写代码一样好理解。
常见的三种异步实现方式:
- 回调函数 Callback
- Promise
- async/await(Promise 的语法糖)
1. 回调函数(Callback)
基本写法
const fs = require('fs');
fs.readFile('./test.txt', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data.toString());
});
运行流程:
-
执行到 fs.readFile 的时候,Node.js 会:
-
把读取文件的任务交给 后台线程(libuv 线程池)。
-
立即返回,让 JS 主线程继续执行后面的代码(非阻塞)。
-
同时把回调函数 (err, data) => {…} 注册 给 Node.js 环境保存起来。
-
-
后台线程读完文件后:
-
如果出错,
err会有值,data是 undefined。 -
如果成功,
err是 null,data是 buffer。 -
Node.js 会把这个回调放入事件循环(Event Loop)的任务队列。
-
-
主线程空闲后,事件循环会取出这个回调执行:
- 回调函数根据 err 判断成败,然后输出文件内容或报错。
原理:
- 把后续处理逻辑写在一个函数里(回调函数)。
- 当异步任务完成后,事件循环会调用该回调。
- 缺点:大量的任务嵌套会形成 回调地狱(Callback Hell),为什么?
- 如果第二个任务必须等第一个任务完成才能执行,就不能直接写成普通的同步顺序。
- 在回调模式下,你只能把第二个任务写在第一个任务的回调里。。
回调地狱示例:
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
// ...
});
});
Mermaid 描述回调地狱:
结构上变成一层层嵌套,难以阅读和维护。
2. Promise
为了解决回调地狱,JavaScript 社区提出了 Promise。
基本写法
const fs = require('fs');
const readFileWithPromise = file => {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};
readFileWithPromise('/task.txt')
.then(data => {
console.log(data.toString());
})
.catch(err => {
console.error(err);
});
Promise 原理简述:
Promise是一个 状态机(Pending → Resolved / Rejected)。- 把原来的回调函数抽象成 “任务完成执行
.then()回调”。 - 错误用
.catch()统一捕获,提高可读性和可维护性。
Promise 调用流程 Mermaid 图:
相比回调方式,Promise 支持链式调用,可以减少嵌套。
3. async / await(Promise 语法糖)
原理:
async函数会隐式返回一个 Promise。await会等待 Promise resolve,然后把结果赋值给变量。- 写起来像同步代码,但不会阻塞线程,底层依然是 Promise + 事件循环。
async function readFileAsync(file) {
try {
const data = await readFileWithPromise(file);
console.log(data.toString());
} catch (err) {
console.error(err);
}
}
readFileAsync('/etc/passwd');
执行流程 Mermaid 图:
这样,代码阅读体验更接近同步,避免了回调嵌套和链式 then 过多的问题。
二、补充讲解:异步和并发的区别
注意本内容是偏向基础方向,这里简单讲解一下啊异步和并发的概念
1. 核心定义
| 概念 | 定义 | 是否同时做多件事 | 举例类比 |
|---|---|---|---|
| 异步 (Asynchronous) |
调用一个任务后不等它完成,先去做别的事,等它完成后再回来处理结果。重点是任务切片调度,不阻塞调用者。 | ❌(单线程下不会同时做多件事,只是错开时间交替执行) | 餐馆点餐:你点了一个菜,服务员去厨房下单,你继续聊天刷手机,菜好了服务员再通知你。 |
| 并行 (Parallel) |
同一时间点真的有多个任务同时进行,通常依赖多核 CPU 或多线程执行。 | ✅ | 餐馆厨房同时有 3 个厨师,每人做不同的菜,真正多个任务物理上同时进行。 |
一句话总结:
异步是编程模式(不等结果先干别的),并行是执行能力(物理上同时做多件事)。
2. 生活类比
- 异步:服务员接到你的菜单 → 把任务交给厨房 → 不等菜好,马上去接其他桌的订单 → 厨房菜做好后通知服务员 → 服务员再端菜给你
(服务员是单线程,异步可以让它更高效地利用时间) - 并行:厨房有三个厨师 → 同时炒三道菜
(多个厨师对应多核CPU)
| 对比维度 | 异步 | 并行 |
|---|---|---|
| 关键特征 | 不等任务完成,先做别的事,任务完成后回调 | 真正同时执行多个任务 |
| 是否需要多线程 | 不需要(单线程也能异步) | 需要多线程或多核 |
| 依赖机制 | 事件循环 + 任务队列 | 多核 CPU、多线程调度 |
| JavaScript 中应用 | setTimeout, fs.readFile, Promise, async/await |
Web Workers, worker_threads, 多进程 |
三、示例对比
🌰 场景设计
我们模拟三个异步任务:
taskC(最后的任务源头)taskB必须等taskC完成后才能执行taskA必须等taskB完成后才能执行
用 setTimeout 模拟每个任务耗时 1 秒。
1️⃣ 回调模式(Callback Hell)
function taskC(callback) {
setTimeout(() => {
console.log('C 完成');
callback(null, '来自 C 的数据');
}, 1000);
}
function taskB(cData, callback) {
setTimeout(() => {
console.log('B 完成,收到:', cData);
callback(null, '来自 B 的数据');
}, 1000);
}
function taskA(bData, callback) {
setTimeout(() => {
console.log('A 完成,收到:', bData);
callback(null, '所有任务结束');
}, 1000);
}
// 回调地狱写法
taskC((err, cData) => {
if (err) return console.error(err);
taskB(cData, (err, bData) => {
if (err) return console.error(err);
taskA(bData, (err, result) => {
if (err) return console.error(err);
console.log(result);
});
});
});
缺点:缩进越来越深,阅读难度大,异常处理重复。
2️⃣ Promise 模式
function taskC() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('C 完成');
resolve('来自 C 的数据');
}, 1000);
});
}
function taskB(cData) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('B 完成,收到:', cData);
resolve('来自 B 的数据');
}, 1000);
});
}
function taskA(bData) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('A 完成,收到:', bData);
resolve('所有任务结束');
}, 1000);
});
}
// 链式调用
taskC()
.then(cData => taskB(cData))
.then(bData => taskA(bData))
.then(result => console.log(result))
.catch(err => console.error(err));
优点:
- 解决了回调地狱的缩进问题(变成链式)
- 错误可以集中到
.catch()捕获
缺点:
- 链式调用多的时候,
.then()会很长 - 参数传递比较麻烦
3️⃣ async/await 模式(Promise 语法糖)
async function runTasks() {
try {
const cData = await taskC();
const bData = await taskB(cData);
const result = await taskA(bData);
console.log(result);
} catch (err) {
console.error(err);
}
}
runTasks();
优点:
- 代码看起来像同步代码,可读性最好
- 异常用
try/catch处理 - 参数传递简单自然
4️⃣ 执行流程对比图
三种模式的核心逻辑完全一样:都是 C → B → A 顺序执行,只是写法不同。
差异主要体现在:回调模式嵌套、Promise 链式、async/await 顺序化语法。
5️⃣ 总结区别
| 模式 | 可读性 | 错误处理 | 缩进 | 参数传递 |
|---|---|---|---|---|
| 回调 | 最差 | 每层都要写 if(err) |
嵌套多 | 嵌套传递 |
| Promise | 较好 | .catch() 统一 |
链式 | .then() 参数 |
| async/await | 最好 | try/catch |
平铺 | 直接赋值 |
四、10道例题
10 道题,覆盖 基础写法、互相转换、执行顺序、事件循环原理、异常处理、并发控制 等不同方面,答案在五里面。
覆盖的知识点
| 题号 | 知识点 |
|---|---|
| 1 | 回调风格,error-first callback |
| 2 | 回调地狱到 Promise 改写 |
| 3 | util.promisify,Promise化 |
| 4 | Promise 链式调用原理 |
| 5 | 宏任务/微任务顺序 |
| 6 | async/await 异常处理 |
| 7 | Promise.all、并发控制 |
| 8 | async/await 顺序依赖 |
| 9 | Promise.race 超时控制 |
| 10 | 事件循环、宏任务、微任务混合执行顺序 |
1. 基础 Callback
题目:
实现一个基于 Node.js 回调风格的函数 readJSONFile(filePath, callback),读取 JSON 文件并解析为对象返回。
要求:
- 使用 Node.js
fs.readFile - 回调遵循 error-first callback 规范:
callback(err, data)
2. 回调地狱识别
题目:
以下代码是典型的回调地狱,请改写成功能等价的 Promise 版本,并有错误处理:
step1(function(a) {
step2(a, function(b) {
step3(b, function(c) {
step4(c, function(result) {
console.log(result);
});
});
});
});
要求:
- 改写成 Promise 链式
- 使用
.catch()来捕捉错误
3. Callback → Promise 转换
题目:
用 Node.js 内置 util.promisify 将以下回调风格 API 转为 Promise,用 async/await 调用:
fs.readFile('./data.txt', (err, data) => {
if (err) return console.error(err);
console.log(data.toString());
});
4. Promise 基础链式调用
题目:
输出以下代码的执行结果,并解释原因:
Promise.resolve(1)
.then(x => x + 1)
.then(x => Promise.resolve(x + 1))
.then(console.log);
5. Promise 与微任务顺序
题目:
分析输出顺序:
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
6. async/await 异常捕捉
题目:
以下代码中,第二个 await 会抛出异常,请用 try/catch 正确处理错误:
async function run() {
const a = await getA();
const b = await getB(); // 这里可能会失败
console.log(b);
}
7. Promise.all 与并发控制
题目:
请用 Promise.all 让 task1、task2、task3 同时执行,等所有结果返回后输出数组结果。
扩展:如果我要求“同一时间最多并发两个任务”,你该如何改?
8. async/await 顺序执行
题目:
让 fetchUser()、fetchOrders()、fetchOrderDetails() 依次执行,后一个依赖前一个的结果,使用 async/await 实现。
9. Promise.race 应用
题目:
实现一个带超时的请求函数 fetchWithTimeout(fetchPromise, ms):
- 如果在
ms毫秒内请求返回,用请求结果 - 否则直接 reject 抛出超时错误
提示:Promise.race
10. 事件循环综合题
题目:
写出以下代码的输出顺序,并画出事件循环执行过程:
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => console.log('4'));
setTimeout(() => {
console.log('5');
}, 0);
console.log('6');
五、完全答案
1. 基础 Callback
题目
实现一个基于 Node.js 回调风格的函数 readJSONFile(filePath, callback),读取 JSON 文件并解析为对象返回。
参考答案
const fs = require('fs');
function readJSONFile(filePath, callback) {
fs.readFile(filePath, 'utf-8', (err, data) => {
if (err) return callback(err);
try {
const obj = JSON.parse(data);
callback(null, obj);
} catch (parseErr) {
callback(parseErr);
}
});
}
// 使用
readJSONFile('./data.json', (err, obj) => {
if (err) return console.error('读取失败:', err);
console.log('读取成功:', obj);
});
要点
- error-first callback 规范
(err, data) - 异常要 try/catch 捕获(JSON.parse 会抛错)
- 适应 Node.js 老式异步 API
2. 回调地狱改 Promise
题目
将嵌套回调改写为 Promise 链式调用。
参考答案
// 原来的回调风格
function step1(cb) { setTimeout(() => cb(1), 500); }
function step2(num, cb) { setTimeout(() => cb(num + 1), 500); }
function step3(num, cb) { setTimeout(() => cb(num + 1), 500); }
function step4(num, cb) { setTimeout(() => cb(num + 1), 500); }
// Promise包装
function step1P() { return new Promise(res => step1(res)); }
function step2P(num) { return new Promise(res => step2(num, res)); }
function step3P(num) { return new Promise(res => step3(num, res)); }
function step4P(num) { return new Promise(res => step4(num, res)); }
step1P()
.then(res1 => step2P(res1))
.then(res2 => step3P(res2))
.then(res3 => step4P(res3))
.then(final => console.log(final))
.catch(console.error);
要点
- 回调地狱的可读性问题 → Promise 的链式调用解决
- 错误
.catch()统一处理
3. Callback → Promise 转换
题目
用 util.promisify 将 fs.readFile 转 Promise,使用 async/await 调用。
参考答案
const fs = require('fs');
const { promisify } = require('util');
const readFileAsync = promisify(fs.readFile);
(async () => {
try {
const content = await readFileAsync('./data.txt', 'utf-8');
console.log(content);
} catch (err) {
console.error(err);
}
})();
要点
util.promisify可将 error-first 回调函数直接变成返回 Promise 的版本- async/await 语法简化调用
4. Promise 链式调用
题目
Promise.resolve(1)
.then(x => x + 1)
.then(x => Promise.resolve(x + 1))
.then(console.log);
输出
3
解析
Promise.resolve(1)状态 Fulfilled,值1- 第一个
.then返回2 - 第二个
.then返回Promise.resolve(3) .then(console.log)输出3
要点
.then可以返回普通值或Promise,都会包装成新的 Promise
5. Promise 与微任务顺序
题目
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
输出
A
D
C
B
解析
- 顺序:
- 同步:
A→D - 微任务:
C - 宏任务:
B
- 同步:
事件循环示意
6. async/await 异常捕捉
题目
async function run() {
const a = await getA();
const b = await getB(); // 可能报错
console.log(b);
}
改写
async function run() {
try {
const a = await getA();
const b = await getB();
console.log(b);
} catch (err) {
console.error('任务失败:', err);
}
}
要点
- async/await 的错误要用
try/catch - async 函数内部抛出的异常会返回一个 rejected Promise
7. Promise.all 并发执行 + 控制并发
题目
并发执行 3 个任务,输出结果数组;额外要求同一时间最多并发 2 个。
参考答案(并发全部)
Promise.all([task1(), task2(), task3()])
.then(results => console.log(results))
.catch(console.error);
附加需求:限制并发
const allTasks = [task1, task2, task3];
let index = 0;
const max = 2; // 最大并发数
function runNext() {
if (index >= allTasks.length) return;
const currentIndex = index;
index++;
allTasks[currentIndex]()
.then(res => {
console.log('完成:', res);
runNext(); // 继续下一个
})
.catch(console.error);
}
for (let i = 0; i < max; i++) {
runNext();
}
要点
Promise.all同步触发多个 Promise- 并发控制要手动调度
8. async/await 顺序执行
题目
async function main() {
const user = await fetchUser();
const orders = await fetchOrders(user.id);
const details = await fetchOrderDetails(orders[0].id);
console.log(details);
}
要点
- await 顺序执行,确保依赖数据传递
- 实际执行是底层 Promise 串行化
9. Promise.race 超时控制
参考答案
function fetchWithTimeout(fetchPromise, ms) {
let timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), ms)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
// 使用:
fetchWithTimeout(fetch('https://example.com'), 3000)
.then(res => console.log('OK'))
.catch(err => console.error(err.message));
要点
Promise.race会取率先完成的 Promise 作为最终结果- 超时 reject 可中断等待
10. 事件循环综合题
题目
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => console.log('4'));
setTimeout(() => {
console.log('5');
}, 0);
console.log('6');
输出
1
6
4
2
3
5
解析步骤
- 同步任务:1 → 6
- 微任务队列:4
- 宏任务队列:
- 第一个 setTimeout 宏任务执行时,输出 2,然后添加一个微任务 3
→ 执行微任务 3 - 第二个 setTimeout 输出 5
- 第一个 setTimeout 宏任务执行时,输出 2,然后添加一个微任务 3
事件循环图
六、JavaScript 异步三种模式互转速查表
好👌,那我就帮你画一个回调(Callback)↔ Promise ↔ async/await 的互相转换速查表,
附上三种写法的模板、互转公式、优缺点对比,一次记牢!
JavaScript 异步三种模式互转速查表
1. 标准模板写法对比
| 模式 | 基础写法(读取文件例子) |
|---|---|
| 回调 Callback | js const fs = require('fs'); fs.readFile('a.txt', 'utf-8', (err, data) => { if (err) return console.error(err); console.log(data); }); |
| Promise | js const fs = require('fs'); const { promisify } = require('util'); const readFileAsync = promisify(fs.readFile); readFileAsync('a.txt', 'utf-8') .then(data => console.log(data)) .catch(err => console.error(err)); |
| async/await | js const fs = require('fs'); const { promisify } = require('util'); const readFileAsync = promisify(fs.readFile); async function run() { try { const data = await readFileAsync('a.txt', 'utf-8'); console.log(data); } catch (err) { console.error(err); } } run(); |
2. 转换公式
📌 回调 → Promise
- 手动包装:
function readFilePromise(path, enc) {
return new Promise((resolve, reject) => {
fs.readFile(path, enc, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
- 或用 Node.js 内置:
const { promisify } = require('util');
const readFilePromise = promisify(fs.readFile);
📌 Promise → async/await
async function run() {
try {
const res = await somePromise();
console.log(res);
} catch (err) {
console.error(err);
}
}
📌 async/await → Promise 链
(就是去掉 await 和 try/catch,换成 .then/.catch)
somePromise()
.then(res => { console.log(res); })
.catch(err => { console.error(err); });
📌 Promise → 回调
(有时老 API 需要回调模式,可以在 Promise 结束后调用回调)
function runWithCallback(callback) {
somePromise()
.then(res => callback(null, res))
.catch(err => callback(err));
}
3. 优缺点对比
| 模式 | 优点 | 缺点 | 典型场景 |
|---|---|---|---|
| 回调 Callback | 兼容性好,最基础的异步形式 | 容易产生回调地狱,错误处理分散 | Node.js 老 API,大量现有库 |
| Promise | 链式调用避免嵌套,统一错误处理 .catch() |
链式调用长可读性差,语法偏冗余 | 并行执行 (Promise.all),清晰的步骤控制 |
| async/await | 最贴近同步写法,可读性最好,异常 try/catch |
只能在 async 函数中用,默认顺序执行 | 顺序依赖的异步,异步流程复杂易读化 |
4. 全局互转示意图 (Mermaid 图)
5. 一眼记住的口诀
- “回调是原始核,Promise 是包装壳,async/await 是易用糖”
- 你永远可以这么想:
- async/await 底层就是 Promise
- Promise 本质还是回调,只是结构更好
- 所有 async/await → Promise → 回调,都能互转
更多推荐



所有评论(0)