# 回调

让我们看一下函数 loadScript(src),该函数使用给定的 src 加载脚本:

function loadScript(src) {
  // 创建一个 <script> 标签,并将其附加到页面
  // 这将使得具有给定 src 的脚本开始加载,并在加载完成后运行
  let script = document.createElement("script");
  script.src = src;
  document.head.append(script);
}

自然情况下,浏览器可能没有时间加载脚本。loadScript 函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。但我们希望了解脚本何时加载完成,以使用其中的新函数和变量。




 



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

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

loadScript("/my/script.js", function () {
  // 在脚本加载完成后,回调函数才会执行
  newFunction(); // 现在它工作了
  // ...
});

# 在回调中回调

我们如何依次加载两个脚本:第一个,然后是第二个?

自然的解决方案是将第二个 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`);
  });
});

如果我们还想要一个脚本呢?不难注意到,每一个新行为(action)都在回调内部。这对于几个行为来说还好,但对于许多行为来说就不好了,所以我们很快就会看到其他变体。

# 处理 Error

在上述示例中,我们并没有考虑出现 error 的情况。如果脚本加载失败怎么办?我们的回调应该能够对此作出反应。




 
 



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 for ${src}`));
  document.head.append(script);
}
loadScript("/my/script.js", function (error, script) {
  if (error) {
    // 处理 error
  } else {
    // 脚本加载成功
  }
});

再次强调,我们在 loadScript 中所使用的方案其实很普遍。它被称为“Error 优先回调(error-first callback)”风格。

约定是:

  • callback 的第一个参数是为 error 而保留的。一旦出现 error,callback(err) 就会被调用。
  • 第二个参数(和下一个参数,如果需要的话)用于成功的结果。此时 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 {
            // ...加载完所有脚本后继续 (*)
          }
        });
      }
    });
  }
});

在上面这段代码中:

  • 我们加载 1.js,如果没有发生错误。
  • 我们加载 2.js,如果没有发生错误。
  • 我们加载 3.js,如果没有发生错误 — 做其他操作 (*)。

如果调用嵌套的增加,代码层次变得更深,维护难度也随之增加,尤其是我们使用的是可能包含了很多循环和条件语句的真实代码,而不是例子中的 ...。

有时这些被称为“回调地狱”或“厄运金字塔”。感受一下下面这个波动拳