回调设计存在几个变体,意在解决一些信任问题(不是全部!)。这种试图从回调模式内部挽救它的意图很好,但却注定不是最好的解决办法。举例来说,为了更好地处理错误,有些 API 设计提供了分离回调(一个用于成功通知,一个用于出错通知):
function success(data) {
console.log( data );
}
function failure(err) {
console.error( err );
}
ajax( "http://some.url.1", success, failure );
在这种设计下, API 的出错处理函数 failure() 常常是可选的,如果没有提供的话,就是假定这个错误可以吞掉。还有一种常见的回调模式叫作“error-first 风格”(有时候也称为“Node 风格”,因为几乎所有 Node.js API 都采用这种风格),其中回调的第一个参数保留用作错误对象(如果有的话)。如果成功的话,这个参数就会被清空 / 置假(后续的参数就是成功数据)。不过,如果产生了错误结果,那么第一个参数就会被置起 / 置真(通常就不会再传递其他结果):
function response(err,data) {
// 出错?
if (err) {
console.error( err );
}
// 否则认为成功
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );
在这两种情况下,都应该注意到以下几点。
首先,这并没有像表面看上去那样真正解决主要的信任问题。这并没有涉及阻止或过滤不想要的重复调用回调的问题。现在事情更糟了,因为现在你可能同时得到成功或者失败的结果,或者都没有,并且你还是不得不编码处理所有这些情况。
另外,不要忽略这个事实:尽管这是一种你可以采用的标准模式,但是它肯定更加冗长和模式化,可复用性不高,所以你还得不厌其烦地给应用中的每个回调添加这样的代码。那么完全不调用这个信任问题又会怎样呢?如果这是个问题的话(可能应该是个问题!),你可能需要设置一个超时来取消事件。可以构造一个工具(这里展示的只是一个“验证概念”版本)来帮助实现这一点:
function timeoutify(fn,delay) {
var intv = setTimeout( function(){
intv = null;
fn( new Error( "Timeout!" ) );
}, delay );
return function() {
// 还没有超时?
if (intv) {
clearTimeout( intv );
fn.apply( this, arguments );
}
};
}
以下是使用方式:
// 使用"error-first 风格" 回调设计
function foo(err,data) {
if (err) {
console.error( err );
}
else {
console.log( data );
}
}
ajax( "http://some.url.1", timeoutify( foo, 500 ) );
还有一个信任问题是调用过早。在特定应用的术语中,这可能实际上是指在某个关键任务完成之前调用回调。但是更通用地来说,对于既可能在现在(同步)也可能在将来(异步)调用你的回调的工具来说,这个问题是明显的。
这种由同步或异步行为引起的不确定性几乎总会带来极大的 bug 追踪难度。在某些圈子里,人们用虚构的十分疯狂的恶魔 Zalgo 来描述这种同步 / 异步噩梦。常常会有“不要放出 Zalgo”这样的呼喊,而这也引出了一条非常有效的建议:永远异步调用回调,即使就在事件循环的下一轮,这样,所有回调就都是可预测的异步调用了。
考虑:
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", result );
a++;
这段代码会打印出 0(同步回调调用)还是 1(异步回调调用)呢?这要视情况而定。你可以看出 Zalgo 的不确定性给 JavaScript 程序带来的威胁。所以听上去有点傻的“不要放出 Zalgo”实际上十分常用,并且也是有用的建议。永远要异步。如果你不确定关注的 API 会不会永远异步执行怎么办呢?可以创建一个类似于这个“验证概念”版本的 asyncify(..) 工具:
function asyncify(fn) {
var orig_fn = fn,
intv = setTimeout( function(){
intv = null;
if (fn) fn();
}, 0 );
fn = null;
return function() {
// 触发太快,在定时器intv触发指示异步转换发生之前?
if (intv) {
fn = orig_fn.bind.apply(
orig_fn,
// 把封装器的this添加到bind(..)调用的参数中,
// 以及克里化(currying)所有传入参数
[this].concat( [].slice.call( arguments ) )
);
}
// 已经是异步
else {
// 调用原来的函数
orig_fn.apply( this, arguments );
}
};
}
可以像这样使用 asyncify(..):
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", asyncify( result ) );
a++;
不管这个 Ajax 请求已经在缓存中并试图对回调立即调用,还是要从网络上取得,进而在将来异步完成,这段代码总是会输出 1,而不是 0——result(..) 只能异步调用,这意味着 a++ 有机会在 result(..) 之前运行。如此,又“解决”了一个信任问题!但这是低效的,而且也会带来膨胀的重复代码,使你的项目变得笨重。
可能现在你希望有内建的 API 或其他语言机制来解决这些问题。那么请继续关注本头条号吧,后期将出一些 ES6 的文章,能帮你很好的解决这一问题!