回调是编写和处理 JavaScript 程序异步逻辑的最常用方式。
回调的信任问题:
思考一下代码:
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", result );
a++;
这段代码会打印出 0(同步回调调用)还是 1(异步回调调用)呢?这要视情况而定。
//转化为异步调用工具
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 );
}
};
}
/**调用代码**/
function result(data) {
console.log( a );
}
var a = 0;
ajax( "..pre-cached-url..", asyncify( result ) );
a++;
不管这个 Ajax 请求已经在缓存中并试图对回调立即调用,还是要从网络上取得,进而在将来 异步完成,这段代码总是会输出 1,而不是 0——result(…) 只能异步调用,这意味着 a++ 有机会在 result(…) 之前运行。
“解决”了一个信任问题!但这是低效的,而且也会带来膨胀的重复代码,使你的项目变得笨重。
总结
回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。
第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码是坏代码,会导致坏 bug。
我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。
可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会被发现。
我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript需要更高级、功能更强大的异步模式。