Skip to main content
 首页 » 编程设计

Javascript回调(一):简述

2022年07月19日130bonelee

Javascript回调(一):简述

Javascript中大多数操作都为异步执行。举例,看看loadScript(src):

function loadScript(src) { 
  let script = document.createElement('script'); 
  script.src = src; 
  document.head.append(script); 
} 

该函数的目的是加载一个新脚本,当把<script src="...">增加至document中,浏览器载入并执行。
我们可以这么使用:

// loads and executes the script 
loadScript('/my/script.js'); 

这个函数是异步执行,因为加载脚本动作不一定立刻完成,但后面会加载完。调用开始加载脚本,然后继续执行。正在加载脚本的同时,下面的代码可能完成执行,如果加载耗时,其他代码也可能同时执行。

loadScript('/my/script.js'); 
// the code below doesn't wait for the script loading to finish 

现在,假设当加载时需使用脚本,可能声明了新的函数,所以我们希望运行函数。
当在loadScript(...)代码后面立刻调用,可能不工作:

loadScript('/my/script.js'); // the script has "function newFunction() {…}" 
 
newFunction(); // no such function! 

自然地,浏览器可能没有时间加载脚本,loadScript函数没有提供一种方式跟踪加载完成事件。最终脚本加载运行,但最好加载好我们能知道,并使用脚本中函数或变量。

让我们增加callback函数作为loadScript函数的第二个参数,当脚本完成加载时执行:

function loadScript(src, callback) { 
  let script = document.createElement('script'); 
  script.src = src; 
 
  script.onload = () => callback(script); 
 
  document.head.append(script); 
} 

现在,如果我们想调用脚本中的函数,我们应该写在callback中:

loadScript('/my/script.js', function() { 
  // the callback runs after the script is loaded 
  newFunction(); // so now it works 
  ... 
}); 

这样理念是:第二个参数通常是函数,当动作完成时运行。这里是一个可运行的真实示例:

function loadScript(src, callback) { 
  let script = document.createElement('script'); 
  script.src = src; 
  script.onload = () => callback(script); 
  document.head.append(script); 
} 
 
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { 
  alert(`Cool, the ${script.src} is loaded`); 
  alert( _ ); // function declared in the loaded script 
}); 

这就是所谓“基于回调”异步编程风格。需要异步处理业务的函数应该提供callback参数,即函数完成之后执行的代码。

这里我们示例loadScript,当然是一个通用的方法。

回调里面的回调

如何顺序载入两个脚本:先载入第一个,然后接着第二个?

自然的解决方案是放第二个载入脚本在第一个回调里,如下方式:

loadScript('/my/script.js', function(script) { 
 
  alert(`Cool, the ${script.src} is loaded, let's load one more`); 
 
  loadScript('/my/script2.js', function(script) { 
    alert(`Cool, the second script is loaded`); 
  }); 
 
}); 

外面的loadScript完成,开始调用里面的,如果有更多的脚本会怎么?

loadScript('/my/script.js', function(script) { 
 
  loadScript('/my/script2.js', function(script) { 
 
    loadScript('/my/script3.js', function(script) { 
      // ...continue after all scripts are loaded 
    }); 
 
  }) 
 
}); 

每个新的动作在回调里面,如果层次不多还好,当层次较多时很不好,所以我们将看其他方式。

处理错误

上面的示例没有考虑错误发生,如果脚本失败怎么?回调应该能够对失败情况有反应。
下面是loadScript的改进版本,跟踪错误发生:

function loadScript(src, callback) { 
  let script = document.createElement('script'); 
  script.src = src; 
 
  script.onload = () => callback(null, script); 
  script.onerror = () => callback(new Error(`Script load error ` + src)); 
 
  document.head.append(script); 
} 

加载成功调用callback(null, script),否则调用callback(error).用法示例:

loadScript('/my/script.js', function(error, script) { 
  if (error) { 
    // handle error 
  } else { 
    // script loaded successfully 
  } 
}); 

实际这样方式使用loadScript很常见,一般称为“错误优先调用”。约定为:

  1. callback的第一个参数用于发生错误时的错误处理逻辑,则callback(err)被调用。
  2. 第二个参数(如果需要可能有多个参数)为成功时调用,那么调用callback(null,result1,result2).

所以一个回调函数既可以报告错误,也可以传回调用结果。

金字塔

第一眼看到可行的异步方式代码,一般一个或两个嵌套调用还好。但多个一个接着一个的异步动作,代码大概如下:

loadScript('1.js', function(error, script) { 
 
  if (error) { 
    handleError(error); 
  } else { 
    // ... 
    loadScript('2.js', function(error, script) { 
      if (error) { 
        handleError(error); 
      } else { 
        // ... 
        loadScript('3.js', function(error, script) { 
          if (error) { 
            handleError(error); 
          } else { 
            // ...continue after all scripts are loaded (*) 
          } 
        }); 
 
      } 
    }) 
  } 
}); 

上面的代码:

  1. 载入1.js,如果没有错误。
  2. 载入2.js,如果没有错误。
  3. 载入3.js,如果么有错误,做其他事情(*号行)

当多个调用嵌套时,代码缩进更深,增加管理难度。特别如果有真实业务代码代替…时,因为可能会包括更多循环、条件语言等。
有时称为“回调地狱”或“末日金字塔”。

嵌套调用金字塔向右增长每个异步动作,很快螺旋方式失去控制。所以这种代码方式不好。

我们能试着解决这个问题,每个动作采用一个独立函数,如下:

loadScript('1.js', step1); 
 
function step1(error, script) { 
  if (error) { 
    handleError(error); 
  } else { 
    // ... 
    loadScript('2.js', step2); 
  } 
} 
 
function step2(error, script) { 
  if (error) { 
    handleError(error); 
  } else { 
    // ... 
    loadScript('3.js', step3); 
  } 
} 
 
function step3(error, script) { 
  if (error) { 
    handleError(error); 
  } else { 
    // ...continue after all scripts are loaded (*) 
  } 
}; 

看到吗?效果一样,但没有深层的嵌套,因为我们分离每个动作作为一个顶级函数。
可以实现,但代码看起来想分裂的电子表格,很难阅读,你可能注意到,需要在不同代码片段间跳跃。确实不方便,特别当读者不属性代码,眼睛不知道往哪跳跃。

以step*方式命名函数为了独立使用,创建他们仅为了消除金字塔,并不打算在外面调用链中重用他们,所以可能会让命名混乱。

我们希望有其他更好的方法。

幸运的是,去有其他方式可以消除金字塔。最好的方式使用“promises”,在下一章进行描述。


本文参考链接:https://blog.csdn.net/neweastsun/article/details/73007127