前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试官:介绍下回调

面试官:介绍下回调

作者头像
公众号---人生代码
发布2021-02-24 11:36:20
5420
发布2021-02-24 11:36:20
举报
文章被收录于专栏:人生代码人生代码

介绍:回调函数

JavaScript主机环境提供了许多函数,允许您调度异步操作。换句话说,我们现在开始的行动,但它们会在稍后结束。

例如,setTimeout函数就是这样一个函数。

在现实世界中还有其他异步操作的例子,例如加载脚本和模块(我们将在后面的章节中介绍)。

看看loadScript(src)函数,它用给定的src加载脚本:

代码语言:javascript
复制
function loadScript(src) {
  // creates a <script> tag and append it to the page
  // this causes the script with given src to start loading and run when complete
  let script = document.createElement('script');
  script.src = src;
  document.head.append(script);
}

它将用给定的src动态创建的新标记添加到文档中。浏览器自动开始加载,并在完成时执行。

我们可以这样使用这个函数:

代码语言:javascript
复制
// load and execute the script at the given path
loadScript('/my/script.js');

脚本是“异步”执行的,因为它现在开始加载,但是在函数已经完成之后运行。

如果在loadScript(…)下面有任何代码,它不会等到脚本加载完成。

代码语言:javascript
复制
loadScript('/my/script.js');
// the code below loadScript
// doesn't wait for the script loading to finish
// ...

假设我们需要在新脚本加载后立即使用它。它声明了新的函数,我们想要运行它们。

但如果我们在调用loadScript(…)之后立即这样做,那就行不通了:

代码语言:javascript
复制
loadScript('/my/script.js'); // the script has "function newFunction() {…}"

newFunction(); // no such function!

406/5000 当然,浏览器可能没有时间加载脚本。到目前为止,loadScript函数还没有提供跟踪加载完成情况的方法。脚本加载并最终运行,仅此而已。但是我们想知道它什么时候发生,从脚本中使用新的函数和变量。

让我们添加一个回调函数作为loadScript的第二个参数,它应该在脚本加载时执行:

代码语言:javascript
复制
function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;

  script.onload = () => callback(script);

  document.head.append(script);
}

现在,如果我们想从脚本中调用新函数,我们应该把它写入回调函数中:

代码语言:javascript
复制
loadScript('/my/script.js', function() {
  // the callback runs after the script is loaded
  newFunction(); // so now it works
  ...
});

这就是它的思想:第二个参数是一个在操作完成时运行的函数(通常是匿名的)。

下面是一个带有真实脚本的可运行的示例:

代码语言:javascript
复制
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 ${script.src} is loaded`);
  alert( _ ); // function declared in the loaded script
});

这就是所谓的“基于回调”的异步编程风格。一个异步执行的函数应该提供一个回调参数,在函数完成后,我们把它放在这里运行。

这里我们是在loadScript中做的,当然这是一种一般的方法。

回调中的回调

我们如何按顺序加载两个脚本:第一个,然后是第二个?

自然的解决方案是将第二个loadScript调用放在回调函数中,像这样:

代码语言:javascript
复制
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。

如果我们还想要一个脚本呢?

代码语言:javascript
复制
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,可以跟踪加载错误:

代码语言:javascript
复制
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);
}

它调用callback(null, script)来成功加载,否则调用callback(error)。

用法:

代码语言:javascript
复制
loadScript('/my/script.js', function(error, script) {
  if (error) {
    // handle error
  } else {
    // script loaded successfully
  }
});

再说一次,我们用于loadScript的配方实际上很常见。它被称为" error-first callback "样式。

惯例是:

回调函数的第一个参数保留给发生错误时使用。然后调用callback(err)。

第二个参数(如果需要,还有下一个参数)用于成功的结果。然后调用callback(null, result1, result2…)。

因此,一个回调函数既用于报告错误,也用于传回结果。

回调地狱

乍一看,这是一种可行的异步编码方式。的确如此。对于一个或两个嵌套调用,它看起来很好。

但是对于一个接一个的多个异步动作,我们会有这样的代码:

代码语言:javascript
复制
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.js,然后,如果没有错误。

我们加载2.js,然后,如果没有错误。

我们加载3.js,然后如果没有错误-做其他事情(*)。

随着调用越来越嵌套,代码变得越来越深,越来越难以管理,特别是如果我们有真正的代码而不是……这可能包括更多的循环、条件语句等等。

这有时被称为"回调地狱"或"末日金字塔"

嵌套调用的“金字塔”随着每个异步操作向右增长。很快它就失控了。

所以这种编码方式不是很好。

我们可以通过将每个操作都变成独立的函数来缓解这个问题,如下所示:

代码语言:javascript
复制
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*的函数都是单一用途,它们只是为了避免“毁灭金字塔”而创建的。“没有人会在行动链之外再使用它们。这里有点命名空间混乱。

(我们想要更好的。)

幸运的是,还有其他方法可以避免这样的金字塔。最好的方法之一是使用“承诺”,这将在下一章中描述。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CryptoCode 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍:回调函数
  • 回调中的回调
  • 错误处理
  • 回调地狱
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档