⛴ JavaScript 回调函数引入
# 更好地理解 JavaScript 中的回调函数
🌰 例子:
封装一个 加载脚本到页面 的函数:
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
2
3
4
5
这个函数使用给定的
src
将一个新的、动态创建的标签<script src="…">
插入到文档中。浏览器将自动开始加载它,并在加载完成后执行它。
使用时,在给定路径下加载并执行脚本:
loadScript('/my/script.js');
但是,脚本是异步调用的。因为它从现在开始加载,但是这个家在函数执行完成后才能运行。
如果 loadScript(...)
之下有 任何其他代码,它们不会等到 加载脚本完成后 才运行,如下:
假设脚本中有函数,想要在加载完成之后,立即执行它:
loadScript('/my/script.js'); // 这个脚本有 "function newFunction() {…}"
newFunction(); // 没有这个函数!
2
3
这样调用脚本里面的函数是没有效果的。因为浏览器可能没有时间家在脚本。并且
loadScript
函数并没有提供跟踪加载完成的方法。脚本加载并最终运行,仅此而已。
要使用脚本中的函数和变量,要了解脚本何时加载完成。给 loadScript
函数 添加第二个参数 作为回调函数,该函数应该在脚本加载完成时执行:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script); //*
document.head.append(script);
}
2
3
4
5
6
7
8
现在,要想调用脚本中的函数就可以在 callback
中写了:
loadScript('/my/script.js', function() {
newFunction();
// ...
});
2
3
4
完整的使用例子:
loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => {
alert(`Cool, the script ${script.src} is loaded`);
alert( _ ); // 所加载的脚本中声明的函数
});
2
3
4
通过这个 「加载脚本」的例子,了解到这是 「基于回调」的一步编程风格。异步执行某项功能的函数,应该提供一个 callback
参数用于在 相应事件完成时 调用。
# 回调中的回调
🌰 延续上面的 加载脚本 例子 / 如果想要在加载完第一个脚本后,加载另一个脚本、或者更多,把下一个加载写在回调中:
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`);
});
});
2
3
4
5
6
7
8
9
谁用例子:
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...加载完所有脚本后继续
});
});
});
2
3
4
5
6
7
8
9
10
11
每一个加载脚本的行为都写在了 回调 中。这样的代码行为其实并不好。
# 回调中出错
🌰 在上面 加载脚本 的例子,并没有处理 加载出错(无法读取文件) 的情况。所以要添加处理错误的步骤:
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);
}
2
3
4
5
6
7
8
9
如果加载成功就运行
callback
。加载失败,就调用callback(error)
。
使用例子:
loadScript('/my/script.js', function(error, script) {
if (error) {
// 处理 error
} else {
// 脚本加载成功
}
});
2
3
4
5
6
7
这种处理错误的风格成为「错误有限回调」(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 {
// ...加载完所有脚本后继续 (*)
}
});
}
});
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
随着代码的嵌套增加,代码层次变得更深层,维护难度提高。
虽然可以通过将每个回调划分出来独立的函数尝试解决这个问题:
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 {
// ...加载完所有脚本后继续 (*)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
但是代码的可读性很差,需要在各个代码块之间跳转。并且这些函数应该是一次性使用的, step
之类的命名带来命名空间的混乱。
要避免 回调地狱 的问题,解决方法最好之一是 Promise 的使用。
# 更多异步编程的例子
异步编程相关例子(回调函数):
fs 文件操作:
require('fs').readFile('./index.html', (err,data)=>{})
1数据库操作;
AJAX 请求:
$.get('/server', (data)=>{})
1定时器:
setTimeout(()=>{}, 2000);
1