介绍:回调函数
JavaScript主机环境提供了许多函数,允许您调度异步操作。换句话说,我们现在开始的行动,但它们会在稍后结束。
例如,setTimeout函数就是这样一个函数。
在现实世界中还有其他异步操作的例子,例如加载脚本和模块(我们将在后面的章节中介绍)。
看看loadScript(src)函数,它用给定的src加载脚本:
它将用给定的src动态创建的新标记添加到文档中。浏览器自动开始加载,并在完成时执行。
我们可以这样使用这个函数:
脚本是“异步”执行的,因为它现在开始加载,但是在函数已经完成之后运行。
如果在loadScript(…)下面有任何代码,它不会等到脚本加载完成。
假设我们需要在新脚本加载后立即使用它。它声明了新的函数,我们想要运行它们。
但如果我们在调用loadScript(…)之后立即这样做,那就行不通了:
406/5000当然,浏览器可能没有时间加载脚本。到目前为止,loadScript函数还没有提供跟踪加载完成情况的方法。脚本加载并最终运行,仅此而已。但是我们想知道它什么时候发生,从脚本中使用新的函数和变量。
让我们添加一个回调函数作为loadScript的第二个参数,它应该在脚本加载时执行:
现在,如果我们想从脚本中调用新函数,我们应该把它写入回调函数中:
这就是它的思想:第二个参数是一个在操作完成时运行的函数(通常是匿名的)。
下面是一个带有真实脚本的可运行的示例:
这就是所谓的“基于回调”的异步编程风格。一个异步执行的函数应该提供一个回调参数,在函数完成后,我们把它放在这里运行。
这里我们是在loadScript中做的,当然这是一种一般的方法。
回调中的回调
我们如何按顺序加载两个脚本:第一个,然后是第二个?
自然的解决方案是将第二个loadScript调用放在回调函数中,像这样:
外部loadScript完成后,回调将启动内部loadScript。
如果我们还想要一个脚本呢?
每个新动作都在回调中。这对于很少的操作来说很好,但是对于很多操作来说就不好了,所以我们将很快看到其他的变体。
错误处理
在上面的例子中,我们没有考虑错误。如果脚本加载失败怎么办?我们的回调应该能够对此做出反应。
这是一个改进版的loadScript,可以跟踪加载错误:
它调用callback(null, script)来成功加载,否则调用callback(error)。
用法:
再说一次,我们用于loadScript的配方实际上很常见。它被称为" error-first callback "样式。
惯例是:
回调函数的第一个参数保留给发生错误时使用。然后调用callback(err)。
第二个参数(如果需要,还有下一个参数)用于成功的结果。然后调用callback(null, result1, result2…)。
因此,一个回调函数既用于报告错误,也用于传回结果。
回调地狱
乍一看,这是一种可行的异步编码方式。的确如此。对于一个或两个嵌套调用,它看起来很好。
但是对于一个接一个的多个异步动作,我们会有这样的代码:
在上述代码中:
我们加载1.js,然后,如果没有错误。
我们加载2.js,然后,如果没有错误。
我们加载3.js,然后如果没有错误-做其他事情(*)。
随着调用越来越嵌套,代码变得越来越深,越来越难以管理,特别是如果我们有真正的代码而不是……这可能包括更多的循环、条件语句等等。
这有时被称为"回调地狱"或"末日金字塔"
嵌套调用的“金字塔”随着每个异步操作向右增长。很快它就失控了。
所以这种编码方式不是很好。
我们可以通过将每个操作都变成独立的函数来缓解这个问题,如下所示:
看到了吗?它做的是一样的,现在没有深度嵌套,因为我们将每个操作都设置为单独的顶级函数。
它可以工作,但代码看起来像一个撕裂的电子表格。它很难阅读,你可能会注意到人们在阅读时需要在各篇文章之间来回切换。这很不方便,特别是如果读者不熟悉代码,不知道眼睛往哪里跳。
同样,名为step*的函数都是单一用途,它们只是为了避免“毁灭金字塔”而创建的。“没有人会在行动链之外再使用它们。这里有点命名空间混乱。
(我们想要更好的。)
幸运的是,还有其他方法可以避免这样的金字塔。最好的方法之一是使用“承诺”,这将在下一章中描述。