为什么回调函数不是一种好的异步编程方式

20 阅读3分钟

为什么对于异步编程,回调不是一种合适的方式,原因在于以下两点:

回调表示异步的方式是非线性的,非顺序的,很难理解

请看以下代码

// 实际实现的复杂回调代码
function registerUser(username, password, callback) {
    // 1. 先检查用户名是否存在
    checkUsernameExists(username, function(err, exists) {
        if (err) {
            return callback(new Error("检查用户名失败: " + err.message));
        }
        
        // 条件判断:如果用户名已存在
        if (exists) {
            return callback(new Error("用户名已存在"));
        }
        
        // 2. 创建新用户
        createUser(username, password, function(err, user) {
            if (err) {
                // 错误处理:创建用户失败
                return callback(new Error("创建用户失败: " + err.message));
            }
            
            // 3. 发送欢迎邮件
            sendWelcomeEmail(user.email, function(err, emailResult) {
                if (err) {
                    // 错误处理:邮件发送失败,需要回滚!
                    console.error("邮件发送失败,回滚用户创建");
                    
                    // 回滚:删除已创建的用户
                    rollbackCreateUser(user.id, function(rollbackErr) {
                        if (rollbackErr) {
                            // 更复杂的错误:回滚也失败了!
                            console.error("回滚失败:", rollbackErr);
                        }
                        
                        // 最终回调,包含原始错误
                        return callback(new Error("发送欢迎邮件失败: " + err.message));
                    });
                    return; // 注意:这里必须 return!
                }
                
                // 4. 记录注册日志(可选操作,但不是必需的)
                if (shouldLogRegistration()) {  // 条件判断
                    logRegistration(user.id, function(err, logResult) {
                        if (err) {
                            // 注意:这里日志失败不影响主流程
                            console.warn("记录日志失败,但继续流程:", err);
                        }
                        
                        // 最终成功回调
                        callback(null, { 
                            user: user, 
                            emailSent: true,
                            logged: !err
                        });
                    });
                } else {
                    // 最终成功回调(不走日志分支)
                    callback(null, { 
                        user: user, 
                        emailSent: true,
                        logged: false
                    });
                }
            });
        });
    });
}

这段代码需要在多个嵌套函数中来回跳跃,而且错误处理代码被分散在了多个层级中,导致这段代码难读之一在于嵌套,之一在于if-else导致代码逻辑进入不同的函数处理中打断了线性的阅读

回调函数的信任问题

回调函数是控制反转的,在使用第三方函数的时候,回调函数把自己函数的一部分执行控制交给第三方函数本身

ajax("url...", function () {
    //逻辑处理
})

当第三方函数如这里的ajax,并没有调用你的回调函数,或者说它并不是你期望的那样是异步的,又或者它把你打断函数比期望中的多调用或少嗲用了几次,都会导致程序出错,但是基于信任问题,我们可以特定的代码去解决一些问题,

调用超时

function timeoutify(fn, delay) {
    var intv = setTimeout(function () {
        intv = null;
        fn(new Error("Timeout !"))
    }, delay);
    
    return function () {
        if (intv) {
            //没有超时
            clearTimeout(intv);
            fn.apply(this,[null].concat([].slice.apply(argument, 0)));
        }
    }
}

function foo (err, data) {
    if (err) {
        console.error(err);
    } else {
        console.log(data);
    }
}

ajax("url...", timeoutify(foo, 500));

调用过早

当不确定使用的api是否会不会永远的异步执行的时候使用

function asyncify (fn) {
    var origin_fn = fn,
        intv = setTimeout(function () {
            intv = null;
            if (fn) fn();
        }, 0);
    fn = null;
    return function () {
        if (intv) {
            //调用过早
            var args = [].slice.call(argument);
            var bindArgs = [this].concat(args);
            
            fn = origin_fn.bind.apply(origin_fn, bindArgs);
            
            
        } else {
            //已经是异步
            orgin_fn.apply(this, argument);
        }
    }
}

这段代码最终能保证你的代码是异步执行的不管在什么情景下,所以尽管可以通过代码去解决一部分的回调信任问题,但是写这种代码的难度要高于业务本身的水平,而且代码也变得更加臃肿和难维护了

总结

基于上述,我们可以知道,为什么异步编程使用回调不是一种好的办法,一部分是回调的表示方式是非线性的非顺序的,一部分是回调函数的控制反转导致的信任问题。