强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第3章:回调函数 —— 最早的异步方案

第3章:回调函数 —— 最早的异步方案

3.1 什么是回调函数?

回调函数(Callback Function)是一个被作为参数传递给另一个函数的函数,在某个操作完成后被**“回调”**执行。

同步回调 vs 异步回调

# 同步回调 — 立即执行
def apply(func, value):
    return func(value)

result = apply(lambda x: x * 2, 21)
print(result)  # 42
// 异步回调 — 延迟执行
fs.readFile('/data.txt', 'utf8', function(err, content) {
    if (err) {
        console.error('读取失败:', err);
        return;
    }
    console.log('文件内容:', content);
});
console.log('文件正在读取中...');
// 输出顺序:
// 文件正在读取中...
// 文件内容: ...

关键区别:异步回调不是在调用时执行,而是在操作完成后的某个时刻由事件循环执行。


3.2 回调地狱(Callback Hell)

当多个异步操作需要按顺序执行时,回调函数会层层嵌套,形成"金字塔"结构,这就是臭名昭著的回调地狱

典型示例

// 用户下单流程:查询用户 → 查询商品 → 创建订单 → 发送通知
function placeOrder(userId, productId, callback) {
    getUser(userId, function(err, user) {
        if (err) return callback(err);

        getProduct(productId, function(err, product) {
            if (err) return callback(err);

            if (product.stock <= 0) {
                return callback(new Error('商品已售罄'));
            }

            createOrder(user, product, function(err, order) {
                if (err) return callback(err);

                sendNotification(user.email, order, function(err) {
                    if (err) return callback(err);

                    callback(null, order);
                });
            });
        });
    });
}

回调地狱的三大问题

问题描述影响
可读性差代码向右不断缩进,形成三角形维护困难,review 成本高
错误处理繁琐每层都要检查 err容易遗漏,导致静默失败
控制流受限无法使用 try/catchfor 循环、return逻辑复杂时代码爆炸

视觉化回调地狱

getUser(userId, function(err, user) {
│   getProduct(productId, function(err, product) {
│   │   createOrder(user, product, function(err, order) {
│   │   │   sendNotification(user.email, order, function(err) {
│   │   │   │   callback(null, order);
│   │   │   });
│   │   });
│   });
});

3.3 Node.js 的 Error-First 回调风格

Node.js 社区建立了统一的回调约定:Error-First Callback(错误优先回调)。

规则

// 约定:回调函数的第一个参数是 error,第二个参数是数据
function asyncOperation(input, callback) {
    // 成功时:callback(null, result)
    // 失败时:callback(error)
}

Node.js 标准库示例

const fs = require('fs');

// 读取文件
fs.readFile('/path/to/file', 'utf8', (err, data) => {
    if (err) {
        // 错误处理
        if (err.code === 'ENOENT') {
            console.error('文件不存在');
        } else if (err.code === 'EACCES') {
            console.error('权限不足');
        } else {
            console.error('未知错误:', err.message);
        }
        return;
    }
    console.log('文件内容:', data);
});

// DNS 查询
const dns = require('dns');
dns.lookup('example.com', (err, address, family) => {
    if (err) throw err;
    console.log('IP 地址:', address);
});

各语言回调风格对比

语言风格示例
Node.jsError-Firstcallback(err, result)
Python异常 + 回调def on_complete(result): / def on_error(e):
Java接口CompletableFuture.thenAccept(result -> {...})
C函数指针void (*callback)(int status, void* data)
Go通常不用回调使用 Channel 或 goroutine

3.4 拯救回调地狱:扁平化技巧

在 Promise 和 async/await 出现之前,社区发明了多种技巧来缓解回调地狱。

技巧一:提取命名函数

// ❌ 匿名回调嵌套
getUser(userId, function(err, user) {
    getProduct(productId, function(err, product) {
        createOrder(user, product, function(err, order) {
            sendNotification(user.email, order, function(err) {
                callback(null, order);
            });
        });
    });
});

// ✅ 提取命名函数
function onGetUser(err, user) {
    if (err) return callback(err);
    getProduct(productId, onGetProduct.bind(null, user));
}

function onGetProduct(user, err, product) {
    if (err) return callback(err);
    createOrder(user, product, onOrderCreated.bind(null, user));
}

function onOrderCreated(user, err, order) {
    if (err) return callback(err);
    sendNotification(user.email, order, onNotified.bind(null, order));
}

function onNotified(order, err) {
    if (err) return callback(err);
    callback(null, order);
}

getUser(userId, onGetUser);

技巧二:使用 async.js 库

const async = require('async');

// 串行执行
async.waterfall([
    (cb) => getUser(userId, cb),
    (user, cb) => getProduct(productId, (err, product) => cb(err, user, product)),
    (user, product, cb) => createOrder(user, product, cb),
    (order, cb) => sendNotification(user.email, order, (err) => cb(err, order)),
], (err, order) => {
    if (err) console.error('下单失败:', err);
    else console.log('下单成功:', order);
});

// 并行执行
async.parallel({
    user: (cb) => getUser(userId, cb),
    product: (cb) => getProduct(productId, cb),
}, (err, results) => {
    // results.user 和 results.product 都就绪
});

技巧三:生成器(Generator)

// 使用 co 库 + Generator(Promise 前的过渡方案)
const co = require('co');

co(function* () {
    const user = yield getUser(userId);
    const product = yield getProduct(productId);
    const order = yield createOrder(user, product);
    yield sendNotification(user.email, order);
    return order;
}).then(order => {
    console.log('下单成功:', order);
}).catch(err => {
    console.error('下单失败:', err);
});

3.5 回调的错误处理陷阱

陷阱一:忘记检查错误

// ❌ 危险:忽略了 err 参数
fs.readFile('/file.txt', 'utf8', (err, data) => {
    console.log(data.length);  // 如果 err 不为 null,data 是 undefined,会崩溃
});

// ✅ 正确:始终检查 err
fs.readFile('/file.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('读取失败:', err);
        return;
    }
    console.log(data.length);
});

陷阱二:多次调用回调

// ❌ 危险:回调可能被调用两次
function dangerousOperation(input, callback) {
    try {
        const result = processData(input);
        callback(null, result);
    } catch (e) {
        callback(e);          // 如果 processData 抛出,callback 被调用
        callback(null, null);  // ← 这行也可能执行!
    }
}

// ✅ 正确:使用标志位防止重复调用
function safeOperation(input, callback) {
    let called = false;
    function safeCallback(...args) {
        if (called) return;
        called = true;
        callback(...args);
    }

    try {
        const result = processData(input);
        safeCallback(null, result);
    } catch (e) {
        safeCallback(e);
    }
}

陷阱三:异常无法被 try/catch 捕获

// ❌ try/catch 无法捕获异步回调中的异常
try {
    fs.readFile('/file.txt', 'utf8', (err, data) => {
        throw new Error('boom!');  // 不会被下面的 catch 捕获
    });
} catch (e) {
    console.error('捕获到:', e);  // 不会执行
}

// ✅ 使用 process.on('uncaughtException') 或 domain
process.on('uncaughtException', (err) => {
    console.error('未捕获异常:', err);
    // 注意:此时进程状态可能不一致,应考虑优雅退出
});

3.6 业务场景:批量文件处理

场景

需要遍历一个目录,读取所有 .json 文件,合并内容,然后写入一个汇总文件。

回调风格实现

const fs = require('fs');
const path = require('path');

function processDirectory(dirPath, callback) {
    fs.readdir(dirPath, (err, files) => {
        if (err) return callback(err);

        const jsonFiles = files.filter(f => f.endsWith('.json'));
        if (jsonFiles.length === 0) {
            return callback(null, []);
        }

        const results = [];
        let pending = jsonFiles.length;
        let hasError = false;

        jsonFiles.forEach(file => {
            const filePath = path.join(dirPath, file);
            fs.readFile(filePath, 'utf8', (err, data) => {
                if (hasError) return;
                if (err) {
                    hasError = true;
                    return callback(err);
                }
                results.push(JSON.parse(data));
                pending--;
                if (pending === 0) {
                    callback(null, results);
                }
            });
        });
    });
}

processDirectory('./data', (err, data) => {
    if (err) {
        console.error('处理失败:', err);
        return;
    }
    console.log('汇总数据:', JSON.stringify(data, null, 2));
});

注意:这段代码已经很复杂了——手动管理计数器、错误标志位。这正是回调模式的痛点。


3.7 回调模式并非一无是处

尽管回调地狱让人痛苦,回调模式本身仍有其价值:

优点说明
底层基础Promise 和 async/await 底层仍然依赖回调机制
极致灵活可以在任何支持函数作为一等公民的语言中使用
零依赖不需要额外的运行时支持
性能好没有 Promise 对象的额外分配和 GC 压力
简单场景对于单次异步操作,回调是最简洁的方式

何时仍然使用回调

// 事件监听器 — 天然的回调场景
server.on('request', (req, res) => {
    res.end('Hello World');
});

// 流式处理 — 回调是自然的选择
stream.on('data', (chunk) => {
    process.stdout.write(chunk);
});

stream.on('end', () => {
    console.log('\n处理完成');
});

stream.on('error', (err) => {
    console.error('流错误:', err);
});

3.8 本章小结

要点说明
回调函数作为参数传递,在操作完成后执行
回调地狱多层嵌套导致可读性差、错误处理繁琐
Error-FirstNode.js 社区的统一回调约定
扁平化技巧命名函数、async.js、Generator
错误处理陷阱忘记检查、多次调用、无法 try/catch
仍然有价值事件监听、流处理、底层机制

下一章预告:回调地狱的痛推动了 Promise 的诞生。下一章我们将学习 Promise 如何用链式调用和统一的错误传播来拯救异步编程。


扩展阅读