Skip to content

[翻译]回调介绍 #4

@nineSean

Description

@nineSean

回调介绍


Javascript中许多动作都是异步的。

举个例子,来看看loadScript(src):

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

这个函数的目的是加载一个新的script。当在document中插入了<script src="...">时,浏览器就会加载并且执行这个脚本。

应用如下:

// 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);
}

如果我们要在脚本中调用一些新的函数,就要在回调中写入:

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-based"风格。一个函数要异步的做些事情,那必须把它作为回调传入另外一个函数,在这个函数做完某些事情后再运行回调。

在这里我们用loadScrip演示,但回调是一个常用的方法。

回调中的回调

如何有序的加载2个脚本:先加载一个,接着再加载第二个?

显然可以通过把第二个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

那么再加一个脚本呢?

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的用法也是非常常见。这叫做"error-first callback"风格。

约定如下:

1.回调的第一个参数是处理错误,然后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,然后如果没有报错就做一些其他的事情(*)

作为嵌套调用,代码变得更深切增加了管理的难度,特别是如果我们有更多的代码代替上面例子中的...,可能是循环,条件等语句A。

这就是所谓的“回调地狱“或者叫做”厄运金字塔“。

callback-hell

随着嵌套回调,如上图"pyramid"在每次异步动作后往右生长,代码逐渐缠绕致使失控。

所以这种代码不好。

我们能够通过声明独立的函数处理每一步动作来缓和问题,如下:

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 (*)
  }
};

看到没?我们做到了,并且不再有深度嵌套了,这是因为我们为每一步动作声明了单独的顶级函数。

虽然实现了功能,但代码却看起来像是撕散割裂的部分。你可能注意到了它难于阅读。必须在2个区块来回切换来阅读理解。这非常的不方便,特别是阅读者不熟悉代码,不知道要哪些区块是功能相关可联系切换的。

另外为命名为step*的函数仅仅单纯地回避了”厄运金字塔“的问题。没有人会在这一系列的动作链之外重复使用它们。所以这会稍微扰乱命名空间。

我们想要更好的方式。

幸运的是有其他的方法避免”厄运金字塔”。其中一个最好的方式就是使用“promises”,详见下章。

任务

用回调动态生成圆

这个任务动态生成圆。

我们不仅需要一个圆,并且在里面显示消息。这个信息需要在动画完成后再显示,不然看起来很丑。

为了完成任务,定义一个`showCircle(cx,cy,radius)`函数画圆,但是没有监控它完成与否。

添加一个回调作为参数:showCircle(cx,cy,radius,callback),当动画完成后调用。这个callback要接受<div>作为参数。

举例如下:

showCircle(150, 150, 100, div => {
  div.classList.add('message-ball');
  div.append("Hello, world!");
});

Demo

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions