为什么对于异步编程,回调不是一种合适的方式,原因在于以下两点:
回调表示异步的方式是非线性的,非顺序的,很难理解
请看以下代码
javascript
// 实际实现的复杂回调代码
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导致代码逻辑进入不同的函数处理中打断了线性的阅读
回调函数的信任问题
回调函数是控制反转的,在使用第三方函数的时候,回调函数把自己函数的一部分执行控制交给第三方函数本身
javascript
ajax("url...", function () {
//逻辑处理
})
当第三方函数如这里的ajax,并没有调用你的回调函数,或者说它并不是你期望的那样是异步的,又或者它把你打断函数比期望中的多调用或少嗲用了几次,都会导致程序出错,但是基于信任问题,我们可以特定的代码去解决一些问题,
调用超时
javascript
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是否会不会永远的异步执行的时候使用
javascript
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);
}
}
}
这段代码最终能保证你的代码是异步执行的不管在什么情景下,所以尽管可以通过代码去解决一部分的回调信任问题,但是写这种代码的难度要高于业务本身的水平,而且代码也变得更加臃肿和难维护了
总结
基于上述,我们可以知道,为什么异步编程使用回调不是一种好的办法,一部分是回调的表示方式是非线性的非顺序的,一部分是回调函数的控制反转导致的信任问题。