很多人(特别是 A 型人)可能不愿意 承认,但我们更多是单任务执行者。实际上,在任何特定的时刻,我们只能思考一件事情。
我们在假装并行执行多个任务时,实际上极有可能是在进行快速的上下文切换,比如与朋 友或家人电话聊天的同时还试图打字。换句话说,我们是在两个或更多任务之间快速连续 地来回切换,同时处理每个任务的微小片段。我们切换得如此之快,以至于对外界来说, 我们就像是在并行地执行所有任务。

执行与计划

“我要去商店,但是路上肯定会接到电话。‘嗨,妈妈。’然后她开始说话的时候, 我要在 GPS 上查找商店的地址,但是 GPS 加载需要几秒钟时间,于是我把收音 机的音量关小,以便听清妈妈讲话。接着我意识到忘了穿外套,外面有点冷,不过没关系,继续开车,继续和妈妈打电话。这时候安全带警告响起,提醒我系好 安全带。‘是的,妈妈,我系着安全带呢。我一直都有系啊!’啊,GPS 终于找 到方向了,于是……”
如果我们这样计划一天中要做什么以及按什么顺序来做的话,事实就会有些荒谬。但是,在实际执行方面,不是多任务,而是快速的上下文切换。
对我们程序员来说,编写异步事件代码,特别是当回调是唯一的实现手段时,困难之处就 在于这种思考 / 计划的意识流对我们中的绝大多数来说是不自然的。

嵌套回调与链式回调

  1. listen( "click", function handler(evt){
  2. setTimeout( function request(){
  3. ajax( "http://some.url.1", function response(text){
  4. if (text == "hello") {
  5. handler();
  6. }
  7. else if (text == "world") {
  8. request();
  9. }
  10. } );
  11. }, 500) ;
  12. } );

这种代码常常被称为回调地狱(callback hell),有时也被称为毁灭金字塔(pyramid of doom,得名于嵌套缩进产生的横向三角形状)。
但实际上回调地狱与嵌套和缩进几乎没有什么关系。它引起的问题要比这些严重得多。
一开始我们在等待 click 事件,然后等待定时器启动,然后等待 Ajax 响应返回,之后可能再重头开始。
一眼看去,这段代码似乎很自然地将其异步性映射到了顺序大脑计划。 首先(现在)我们有:

  1. listen( "..", function handler(..){
  2. // ..
  3. } );

然后是将来,我们有:

  1. setTimeout( function request(..){
  2. // ..
  3. }, 500) ;

接着还是将来,我们有:

  1. ajax( "..", function response(..){
  2. // ..
  3. } );

最后(最晚的将来),我们有:

  1. if ( .. ) {
  2. // ..
  3. }
  4. else ..

但以这种方式线性地追踪这段代码还有几个问题。
首先,例子中的步骤是按照 1、2、3、4……的顺序,这只是一个偶然。实际的异步 JavaScript 程序中总是有很多噪声,使得代码更加杂乱。在大脑的演习中,我们需要熟练地绕过这些噪声,从一个函数跳到下一个函数。对于这样满是回调的代码,理解其中的异步流不是不可能,但肯定不自然,也不容易,即使经过大量的练习也是如此。

  1. doA( function(){
  2. doB();
  3. doC( function(){
  4. doD();
  5. })
  6. doE(); } );
  7. doF();

实际运行顺序是这样的:

  • doA()
  • doF()
  • doB()
  • doC()
  • doE()
  • doD()

较为真实案例:

  1. listen( "click", handler );
  2. function handler() {
  3. setTimeout( request, 500 );
  4. }
  5. function request(){
  6. ajax( "http://some.url.1", response );
  7. }
  8. function response(text){
  9. if (text == "hello") {
  10. handler(); }
  11. else if (text == "world") {
  12. request();
  13. }
  14. }

如上案例,在线性(顺序)地追踪这段代码的过程中,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以“查看”流程。而且别忘了,这还是简化的形式,只考虑了最优情况。我们都知道,真实的异步 JavaScript 程序代码要混乱得多,这使得这种追踪的难度会成倍增加。
如果上述代码中步骤 2 失败,就永远不会到达步骤 3,不管是重试步骤 2,还是跳转到其他错误处理流程,处理起来都是相对来说繁琐,而且代码也会变的无法复用,这是回调地狱的真正问题所在!嵌套和缩进基本上只是转移注意力的枝节而已。

回调最大的问题是控制反转,它会导致信任链的完全断裂。如果你的代码中使用了回调,尤其是但也不限于使用第三方工具,而且你还没有应用某种 逻辑来解决所有这些控制反转导致的信任问题,那你的代码现在已经有了 bug,即使它们还没有给你造成损害。隐藏的 bug 也是 bug。