很多人(特别是 A 型人)可能不愿意 承认,但我们更多是单任务执行者。实际上,在任何特定的时刻,我们只能思考一件事情。
我们在假装并行执行多个任务时,实际上极有可能是在进行快速的上下文切换,比如与朋 友或家人电话聊天的同时还试图打字。换句话说,我们是在两个或更多任务之间快速连续 地来回切换,同时处理每个任务的微小片段。我们切换得如此之快,以至于对外界来说, 我们就像是在并行地执行所有任务。
执行与计划
“我要去商店,但是路上肯定会接到电话。‘嗨,妈妈。’然后她开始说话的时候, 我要在 GPS 上查找商店的地址,但是 GPS 加载需要几秒钟时间,于是我把收音 机的音量关小,以便听清妈妈讲话。接着我意识到忘了穿外套,外面有点冷,不过没关系,继续开车,继续和妈妈打电话。这时候安全带警告响起,提醒我系好 安全带。‘是的,妈妈,我系着安全带呢。我一直都有系啊!’啊,GPS 终于找 到方向了,于是……”
如果我们这样计划一天中要做什么以及按什么顺序来做的话,事实就会有些荒谬。但是,在实际执行方面,不是多任务,而是快速的上下文切换。
对我们程序员来说,编写异步事件代码,特别是当回调是唯一的实现手段时,困难之处就 在于这种思考 / 计划的意识流对我们中的绝大多数来说是不自然的。
嵌套回调与链式回调
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} );
这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔(pyramid of doom,得名于嵌套缩进产生的横向三角形状)。
但实际上回调地狱与嵌套和缩进几乎没有什么关系。它引起的问题要比这些严重得多。
一开始我们在等待 click 事件,然后等待定时器启动,然后等待 Ajax 响应返回,之后可能再重头开始。
一眼看去,这段代码似乎很自然地将其异步性映射到了顺序大脑计划。 首先(现在)我们有:
listen( "..", function handler(..){
// ..
} );
然后是将来,我们有:
setTimeout( function request(..){
// ..
}, 500) ;
接着还是将来,我们有:
ajax( "..", function response(..){
// ..
} );
最后(最晚的将来),我们有:
if ( .. ) {
// ..
}
else ..
但以这种方式线性地追踪这段代码还有几个问题。
首先,例子中的步骤是按照 1、2、3、4……的顺序,这只是一个偶然。实际的异步 JavaScript 程序中总是有很多噪声,使得代码更加杂乱。在大脑的演习中,我们需要熟练地绕过这些噪声,从一个函数跳到下一个函数。对于这样满是回调的代码,理解其中的异步流不是不可能,但肯定不自然,也不容易,即使经过大量的练习也是如此。
doA( function(){
doB();
doC( function(){
doD();
})
doE(); } );
doF();
实际运行顺序是这样的:
- doA()
- doF()
- doB()
- doC()
- doE()
- doD()
较为真实案例:
listen( "click", handler );
function handler() {
setTimeout( request, 500 );
}
function request(){
ajax( "http://some.url.1", response );
}
function response(text){
if (text == "hello") {
handler(); }
else if (text == "world") {
request();
}
}
如上案例,在线性(顺序)地追踪这段代码的过程中,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以“查看”流程。而且别忘了,这还是简化的形式,只考虑了最优情况。我们都知道,真实的异步 JavaScript 程序代码要混乱得多,这使得这种追踪的难度会成倍增加。
如果上述代码中步骤 2 失败,就永远不会到达步骤 3,不管是重试步骤 2,还是跳转到其他错误处理流程,处理起来都是相对来说繁琐,而且代码也会变的无法复用,这是回调地狱的真正问题所在!嵌套和缩进基本上只是转移注意力的枝节而已。
回调最大的问题是控制反转,它会导致信任链的完全断裂。如果你的代码中使用了回调,尤其是但也不限于使用第三方工具,而且你还没有应用某种 逻辑来解决所有这些控制反转导致的信任问题,那你的代码现在已经有了 bug,即使它们还没有给你造成损害。隐藏的 bug 也是 bug。