# 简介:回调
JavaScipt 中的许多动作都是**异步**的。
比如,这个`loadScript(src)`函数:
~~~js
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
~~~
这个函数的作用是加载一个新的脚本。当使用`<script src="…">`将其添加到文档中时,浏览器就会对它进行加载和执行。
我们可以像这样使用:
~~~js
// 加载并执行脚本
loadScript('/my/script.js');
~~~
函数是**异步**调用的,因为动作不是此刻(加载脚本)完成的,而是之后。
调用初始化脚本加载,然后继续执行。当脚本正在被加载时,下面的代码可能已经完成了执行,如果加载需要时间,那么同一时间,其他脚本可能也会被运行。
~~~js
loadScript('/my/script.js');
// 下面的代码在加载脚本时,不会等待脚本被加载完成
// ...
~~~
现在,我们假设想在新脚本被加载完成时,被立即使用。它可能声明了新函数,因此我们想要运行它们。
但如果我们在`loadScript(…)`调用后,立即那么做,就会导致操作失败。
~~~js
loadScript('/my/script.js'); // 脚本含有 "function newFunction() {…}"
newFunction(); // 没有这个函数!
~~~
很明显,浏览器没有时间去加载脚本。因此,对新函数的立即调用失败了。`loadScript`函数并没有提供追踪加载完成时方法。脚本加载然后最终的运行,仅此而已。但我们希望了解脚本何时加载完成,以使用其中的新函数和新变量。
我们将`callback`函数作为第二个参数添加至`loadScript`中,函数在脚本加载时被执行:
~~~js
function loadScript(src, *!*callback*/!*) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
~~~
如果现在你想从脚本中调用新函数,我们应该在回调函数中那么写:
~~~js
loadScript('/my/script.js', function() {
// 在脚本被加载后,回调才会被运行
newFunction(); // 现在起作用了
...
});
~~~
这是我们的想法:第二个参数是一个函数(通常是匿名的)会在动作完成后被执行。
这是一个可运行的真实脚本示例:
~~~js
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( _ ); // 在加载的脚本中声明的函数
});
*/!*
~~~
这被称为“基于回调”的异步编程风格。异步执行某些动作的函数,应该提供一个在函数完成时可以运行的`callback`参数。
我们`loadScript`中就是那么做的,但很明显这是一般性的方法。
## [](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/article.md#%E5%9C%A8%E5%9B%9E%E8%B0%83%E4%B8%AD%E5%9B%9E%E8%B0%83)在回调中回调
如何顺序加载两个脚本:先是第一个,然后是第二个?
最明显的方法是将第二个`loadScript`调用放在回调中,就像这样:
~~~js
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`完成时,内部回调就会被回调。
如果我们还想要一个脚本呢?
~~~js
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
*!*
loadScript('/my/script3.js', function(script) {
// ...在所有脚本被加载后继续操作
});
*/!*
})
});
~~~
因此,每一个动作都在回调内部。这对于新动作来说,非常好,但是其他动作却并不友好,因此我们接下来会看到一些此方法的变体。
## [](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/article.md#%E5%A4%84%E7%90%86%E9%94%99%E8%AF%AF)处理错误
上述示例中,我们并没有考虑错误因素。假如加载失败会如何?我们的回调应该可以立即对其做出响应。
这是可以跟踪错误的`loadScript`改进版:
~~~js
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)`。
用法:
~~~js
loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// 成功加载脚本
}
});
~~~
再一次强调,我们使用的`loadScript`方法是非常常规的。它被称为 "error-first callback" 风格。
惯例是:
1. `callback`的第一个参数是为了错误发生而保留的。一旦发生错误,`callback(err)`就会被调用。
2. 第二个参数(如果有需要)用于成功的结果。此时`callback(null, result1, result2…)`将被调用。
因此单个`callback`函数可以同时具有报告错误以及传递返回结果的作用。
## [](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/article.md#%E5%9B%9E%E8%B0%83%E9%87%91%E5%AD%97%E5%A1%94)回调金字塔
从第一步可以看出,这是异步编码的一种可行性方案。的确如此,对于一个或两个的简单嵌套,这样的调用看起来非常好。
但对于一个接一个的多个异步动作,代码就会变成这样:
~~~js
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. 我们加载`1.js`,如果没有发生错误。
2. 我们加载`2.js`,如果没有发生错误。
3. 我们加载`3.js`,如果没有发生错误 —— 做其他操作`(*)`。
如果嵌套变多,代码层次就会变深,维护难度也随之增加,尤其是如果我们有一个不是`...`的真实代码,就会包含更多的循环,条件语句等。
这有时称为“回调地狱”或者“回调金字塔”。
[![](https://github.com/javascript-tutorial/zh.javascript.info/raw/master/1-js/11-async/01-callbacks/callback-hell.png)](https://github.com/javascript-tutorial/zh.javascript.info/blob/master/1-js/11-async/01-callbacks/callback-hell.png)
嵌套调用的“金字塔”在每一个异步动作中都会向右增长。很快就会失去控制。
因此这种编码方式并不可取。
我们可以通过为每个动作编写一个独立函数来解决这一问题,就像这样:
~~~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 {
// ...在所有脚本被加载后继续 (*)
}
};
~~~
看到了么?效果一样,但是没有深层的嵌套了,因为我们使每个动作都有一个独立的顶层函数。
这很有效,但代码看起来就像是一个被分裂的表格。你可能注意到了,它的可读性非常差。在阅读时,需要在块之间切换。这非常不方便,尤其是不熟悉代码的读者,他们甚至不知道该跳转到何处。
名为`step*`的函数都是单一使用的,他们被创建的唯一作用就是避免“回调金字塔”。没有人会在动作链之外重复使用它们。因此这里的命名空间非常杂乱。
或许还有更好的方法。
幸运地是,有其他方法可以避免回调金字塔。其中一个最好的方法是使用 "promises",我们将在下一章中详细描述。
- 内容介绍
- EcmaScript基础
- 快速入门
- 常量与变量
- 字符串
- 函数的基本概念
- 条件判断
- 数组
- 循环
- while循环
- for循环
- 函数基础
- 对象
- 对象的方法
- 函数
- 变量作用域
- 箭头函数
- 闭包
- 高阶函数
- map/reduce
- filter
- sort
- Promise
- 基本对象
- Arguments 对象
- 剩余参数
- Map和Set
- Json基础
- RegExp
- Date
- async
- callback
- promise基础
- promise-api
- promise链
- async-await
- 项目实践
- 标签系统
- 远程API请求
- 面向对象编程
- 创建对象
- 原型继承
- 项目实践
- Classes
- 构造函数
- extends
- static
- 项目实践
- 模块
- import
- export
- 项目实践
- 第三方扩展库
- immutable
- Vue快速入门
- 理解MVVM
- Vue中的MVVM模型
- Webpack+Vue快速入门
- 模板语法
- 计算属性和侦听器
- Class 与 Style 绑定
- 条件渲染
- 列表渲染
- 事件处理
- 表单输入绑定
- 组件基础
- 组件注册
- Prop
- 自定义事件
- 插槽
- 混入
- 过滤器
- 项目实践
- 标签编辑
- iView
- iView快速入门
- 课程讲座
- 环境配置
- 第3周 Javascript快速入门