一、核心讲解

背景知识

JavaScript 是 单线程事件循环机制,所以必须支持异步编程才能提高运行效率。
异步编程的语法目标:让异步过程能像同步写代码一样好理解。

常见的三种异步实现方式:

  1. 回调函数 Callback
  2. Promise
  3. 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());
});

运行流程:

  1. 执行到 fs.readFile 的时候,Node.js 会:

    • 把读取文件的任务交给 后台线程(libuv 线程池)。

    • 立即返回,让 JS 主线程继续执行后面的代码(非阻塞)。

    • 同时把回调函数 (err, data) => {…} 注册 给 Node.js 环境保存起来。

  2. 后台线程读完文件后:

    • 如果出错,err 会有值,data 是 undefined。

    • 如果成功,err 是 null,data 是 buffer。

    • Node.js 会把这个回调放入事件循环(Event Loop)的任务队列

  3. 主线程空闲后,事件循环会取出这个回调执行:

    • 回调函数根据 err 判断成败,然后输出文件内容或报错。

原理

  • 把后续处理逻辑写在一个函数里(回调函数)。
  • 当异步任务完成后,事件循环会调用该回调。
  • 缺点:大量的任务嵌套会形成 回调地狱(Callback Hell),为什么?
    • 如果第二个任务必须等第一个任务完成才能执行,就不能直接写成普通的同步顺序。
    • 在回调模式下,你只能把第二个任务写在第一个任务的回调里。。

回调地狱示例

fs.readFile(fileA, function (err, data) {
    fs.readFile(fileB, function (err, data) {
        // ...
    });
});

Mermaid 描述回调地狱:

开始
读取文件A
回调函数1
读取文件B
回调函数2
结束

结构上变成一层层嵌套,难以阅读和维护。


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对象 文件系统 new Promise(提供resolve/reject) 异步读取文件 成功(resolve) 或 失败(reject) then() 或 catch() 调用方 Promise对象 文件系统

相比回调方式,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 图:

主线程 async函数 Promise FS 调用 async函数() 调用 Promise(等待) 异步读取文件 resolve(data) 返回 data 执行后续代码 主线程 async函数 Promise FS

这样,代码阅读体验更接近同步,避免了回调嵌套和链式 then 过多的问题。


二、补充讲解:异步和并发的区别

注意本内容是偏向基础方向,这里简单讲解一下啊异步和并发的概念


1. 核心定义

概念 定义 是否同时做多件事 举例类比
异步
(Asynchronous)
调用一个任务后不等它完成,先去做别的事,等它完成后再回来处理结果。重点是任务切片调度,不阻塞调用者。 ❌(单线程下不会同时做多件事,只是错开时间交替执行) 餐馆点餐:你点了一个菜,服务员去厨房下单,你继续聊天刷手机,菜好了服务员再通知你。
并行
(Parallel)
同一时间点真的有多个任务同时进行,通常依赖多核 CPU 或多线程执行。 餐馆厨房同时有 3 个厨师,每人做不同的菜,真正多个任务物理上同时进行。

一句话总结

异步是编程模式(不等结果先干别的),并行是执行能力(物理上同时做多件事)。


2. 生活类比

  • 异步:服务员接到你的菜单 → 把任务交给厨房 → 不等菜好,马上去接其他桌的订单 → 厨房菜做好后通知服务员 → 服务员再端菜给你
    (服务员是单线程,异步可以让它更高效地利用时间)
  • 并行:厨房有三个厨师 → 同时炒三道菜
    (多个厨师对应多核CPU)

对比维度 异步 并行
关键特征 不等任务完成,先做别的事,任务完成后回调 真正同时执行多个任务
是否需要多线程 不需要(单线程也能异步) 需要多线程或多核
依赖机制 事件循环 + 任务队列 多核 CPU、多线程调度
JavaScript 中应用 setTimeout, fs.readFile, Promise, async/await Web Workers, worker_threads, 多进程

三、示例对比


🌰 场景设计

我们模拟三个异步任务:

  1. taskC(最后的任务源头)
  2. taskB 必须等 taskC 完成后才能执行
  3. 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️⃣ 执行流程对比图

开始
执行 taskC
执行 taskB
执行 taskA
结束

三种模式的核心逻辑完全一样:都是 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.alltask1task2task3 同时执行,等所有结果返回后输出数组结果。

扩展:如果我要求“同一时间最多并发两个任务”,你该如何改?


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.promisifyfs.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

解析

  • 顺序:
    1. 同步:AD
    2. 微任务:C
    3. 宏任务:B

事件循环示意

主线程执行同步
执行微任务队列.then
执行宏任务队列 setTimeout

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. 同步任务:1 → 6
  2. 微任务队列:4
  3. 宏任务队列:
    • 第一个 setTimeout 宏任务执行时,输出 2,然后添加一个微任务 3
      → 执行微任务 3
    • 第二个 setTimeout 输出 5

事件循环图

Main Micro Macro 执行同步 1 添加 setTimeout(2),setTimeout(5) 添加 Promise.then(4) 执行同步 6 执行微任务 4 执行宏任务(2)→添加微任务(3) 执行微任务 3 执行宏任务(5) Main Micro Macro

六、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 链

(就是去掉 awaittry/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 图)

util.promisify / new Promise
async/await 语法糖
改写成 then/catch
封装 callback 调用
回调 Callback
Promise
async/await

5. 一眼记住的口诀

  • “回调是原始核,Promise 是包装壳,async/await 是易用糖”
  • 你永远可以这么想:
    • async/await 底层就是 Promise
    • Promise 本质还是回调,只是结构更好
    • 所有 async/await → Promise → 回调,都能互转

更多推荐