mirror of
https://github.com/qianguyihao/Web.git
synced 2024-11-01 21:44:45 +08:00
245 lines
10 KiB
Markdown
245 lines
10 KiB
Markdown
---
|
||
title: 05-回调函数
|
||
---
|
||
|
||
<ArticleTopAd></ArticleTopAd>
|
||
|
||
|
||
我们在前面的文章《JavaScript 基础:异步编程/单线程和异步》中讲过,Javascript 是⼀⻔单线程语⾔。早期我们解决异步场景时,⼤部分情况都是通过回调函数来进⾏。
|
||
|
||
(如果你还不了解单线程和异步的概念,可以先去回顾上一篇文章。)
|
||
|
||
## 回调函数的定义
|
||
|
||
把函数 A 传给另一个函数 B 调用,那么函数 A 就是回调函数。
|
||
|
||
例如在浏览器中发送 ajax 网络请求,或者在定时器中执行异步任务,就是最常⻅的异步场景。发送请求后,需要等待一段时间,等服务端响应之后我们才能拿到结果。如果我们希望**等待异步任务结束之后再执⾏想要的操作**,就只能通过**回调函数**这样的⽅式进⾏处理。
|
||
|
||
```js
|
||
const dynamicFunc = function (callback) {
|
||
setTimeout(function () {
|
||
console.log("一开始在这里执行异步任务 task1,延迟3秒执行");
|
||
// task1: total 计数
|
||
let total = 0;
|
||
for (let i = 0; i < 10; i++) {
|
||
total += i;
|
||
}
|
||
|
||
// 等待异步任务 task1 执行完成后,通过回调传入的 callback() 函数,通知外面的调用者,可以开始做后续任务 task2 了
|
||
// 如果有需要的话,可以把 task1 的执行结果 total 传给外面。
|
||
callback && callback(total);
|
||
}, 3000);
|
||
};
|
||
|
||
// 执行同步任务 task2。需要先等 异步任务 task1做完。
|
||
dynamicFunc(function (value) {
|
||
console.log("外面监听到,异步任务 task1已经完成了,并且还能拿到 task1的执行结果 value");
|
||
console.log("task1的返回值value:" + value);
|
||
|
||
// task2:将task1的执行结果乘以2
|
||
const result = value * 2;
|
||
console.log("result:" + result);
|
||
});
|
||
```
|
||
|
||
上⾯的例⼦中,dynamicFunc() 函数里面的 setTimeout()就是⼀个异步函数,在里面执行了一些异步任务,延迟3秒执行。dynamicFunc() 的参数 callback() 就是一个回调函数。这段代码的诉求是:**先等待 异步任务 task1 做完,再做 同步任务task2**。我们来分析一下。
|
||
|
||
已知异步任务 task1 需要3秒才能做完。**3秒结束后,通知 dynamicFunc 函数的调用者,里面的异步任务 task1 已经做完了,外面可以开始做后续的任务 task2 了**。那要怎么通知呢?在ES5中,最常见的做法就是**需要回调传入的 callback 函数**(也就是回调函数), 通知外面的调用者。并且,如果有需要的话,外面还可以拿到异步任务task1的执行结果 total(详见代码注释)。
|
||
|
||
(注:`callback`这个单词并不是关键字,可以自由命名,我们通常习惯性地用“回调”这个词的英文名 callback 代表回调函数。)
|
||
|
||
## 回调函数的异常处理
|
||
|
||
实际开发中,为什么会经常存在异步任务呢?这是因为,有很多函数在执行时无法立即完成,我们也不知道它什么时候能完成。但是,我们需要等待它完成后,才能做接下来的事情。换句话说,我们接下来要做的事情,需要依赖此前的异步任务。
|
||
|
||
比如, ajax 网络请求就是典型的异步任务。在渲染一个页面时,我们需要请求接口,获取页面所需要的数据。等接口请求完成、数据准备好之后,前端就可以对数据进行处理,并将数据渲染到页面了。前端做的这部分事情,就是在回调函数里面做。
|
||
|
||
当然,异步任务在执行时可能出现异常、错误信息、执行失败等等。当出现异常时,往往导致后续的回调函数无法执行。这就需要在异步任务中将异常信息通知给外部。
|
||
|
||
代码举例如下:
|
||
|
||
```js
|
||
// 封装异步任务
|
||
const dynamicFunc = function (number, successCallback, failureCallback) {
|
||
setTimeout(function () {
|
||
console.log('一开始在这里执行异步任务 task1,延迟3秒执行');
|
||
let total = 0;
|
||
for (let i = 0; i < 10; i++) {
|
||
total += i;
|
||
}
|
||
if (number > 0) {
|
||
// 异步任务执行成功
|
||
successCallback && successCallback(total);
|
||
} else {
|
||
// 异步任务执行鼠标
|
||
failureCallback && failureCallback('异步任务执行失败');
|
||
}
|
||
}, 3000);
|
||
};
|
||
|
||
// 执行异步任务:等待 异步任务 执行完成后,再执行回调函数。
|
||
dynamicFunc(
|
||
100,
|
||
(value) => {
|
||
console.log('异步函数调用成功:' + value);
|
||
// task2:将task1的执行结果乘以2
|
||
const result = value * 2;
|
||
console.log('result:' + result);
|
||
},
|
||
(err) => {
|
||
console.log('异步函数调用失败:', err);
|
||
}
|
||
);
|
||
```
|
||
|
||
## 处理异步任务的基本模型(ES5写法)
|
||
|
||
我们以“发送网络请求”为例,通过回调函数处理异步任务时,既有请求成功的情况,也有请求失败的情况。其基本处理模型如下:
|
||
|
||
(1)调用一个异步函数,在这个函数中发送网络请求(也可以用定时器来模拟异步任务)。
|
||
|
||
(2)如果网络请求成功,则告知调用者请求成功,并将接口返回的数据传递出去。
|
||
|
||
(3) 如果网络请求失败,则告知调用者发送失败,并将错误信息传递出去。
|
||
|
||
ES5中,回调函数处理异步任务的基本代码结构如下:
|
||
|
||
```js
|
||
// ES5中,使用传统的回调函数,处理异步任务的基本模型
|
||
|
||
// 封装异步任务
|
||
function requestData(url, successCallback, failureCallback) {
|
||
const res = {
|
||
retCode: 0,
|
||
data: 'qiangu yihao`s data',
|
||
errMsg: 'network is error',
|
||
};
|
||
setTimeout(() => {
|
||
if (res.retCode == 0) {
|
||
// 网络请求成功
|
||
successCallback(res.data);
|
||
} else {
|
||
// 网络请求失败
|
||
failureCallback(res.errMsg);
|
||
}
|
||
}, 1000);
|
||
}
|
||
|
||
// 调用异步任务
|
||
requestData(
|
||
'www.qianguyihao.com/xxx',
|
||
data => {
|
||
console.log('异步任务执行成功:', data);
|
||
},
|
||
err => {
|
||
console.log('异步任务执行失败:', err);
|
||
}
|
||
);
|
||
```
|
||
|
||
我们一定要记住这个处理模型,它我们学习异步编程的范式之一。如果前端接下来要做的事情需要依赖这个异步任务、需要等待这个异步任务做完之后才能继续,那就符合上面的处理模型。
|
||
|
||
## ES5中,回调的缺点(异步代码的困境)
|
||
|
||
上面的回调函数的写法,都是ES5的写法。ES5中回调的写法比较直观,不需要 return,层层嵌套即可。但也存在两个问题:
|
||
|
||
- 1. 如果嵌套过深,则会出现**回调地狱**的问题。
|
||
|
||
- 2. 不同的异步函数,回调的参数,在写法上可能不一致,导致不规范、且需要**单独记忆**。
|
||
|
||
我们来具体看看这两个问题。
|
||
|
||
### 1、回调地狱的问题
|
||
|
||
如果多个异步函数存在依赖关系(比如,需要等第一个异步函数执行完成后,才能执行第二个异步函数;等第二个异步函数执行完毕后,才能执行第三个异步函数),就需要多个异步函数进⾏层层嵌套,⾮常不利于后续的维护,而且会导致**回调地狱**的问题。
|
||
|
||
关于回调地狱,我们来举一个形象的例子:
|
||
|
||
> 假设买菜、做饭、洗碗、倒厨余垃圾都是异步的。
|
||
|
||
> 但真实的场景中,实际的操作流程是:买菜成功之后,才能开始做饭。做饭成功后,才能开始洗碗。洗碗完成后, 再倒厨余垃圾。这里的一系列动作就涉及到了多层嵌套调用,也就是回调地狱。
|
||
|
||
关于回调地狱,我们来看看几段代码举例。
|
||
|
||
1.1、定时器的代码举例:(回调地狱)
|
||
|
||
```js
|
||
setTimeout(function () {
|
||
console.log('qiangu1');
|
||
setTimeout(function () {
|
||
console.log('qiangu2');
|
||
setTimeout(function () {
|
||
console.log('qiangu3');
|
||
}, 3000);
|
||
}, 2000);
|
||
}, 1000);
|
||
```
|
||
|
||
1.2、Node.js 读取文件的代码举例:(回调地狱)
|
||
|
||
```js
|
||
fs.readFile(A, 'utf-8', function (err, data) {
|
||
fs.readFile(B, 'utf-8', function (err, data) {
|
||
fs.readFile(C, 'utf-8', function (err, data) {
|
||
fs.readFile(D, 'utf-8', function (err, data) {
|
||
console.log('qianguyihao:' + data);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
上面代码的逻辑为:先读取 A 文本内容,再根据 A 文本内容读取 B,然后再根据 B 的内容读取 C。为了实现这个业务逻辑,上面的代码就很容易形成回调地狱。
|
||
|
||
1.3、ajax 请求的代码举例:(回调地狱)
|
||
|
||
```js
|
||
// 伪代码
|
||
ajax('a.json', (res1) => {
|
||
console.log(res1);
|
||
ajax('b.json', (res2) => {
|
||
console.log(res2);
|
||
ajax('c.json', (res3) => {
|
||
console.log(res3);
|
||
});
|
||
});
|
||
});
|
||
```
|
||
|
||
### 2、回调写法不一致的问题
|
||
|
||
我们需要自己去设计回调函数,包括回调函数的参数格式 、调用方式等等。
|
||
|
||
```js
|
||
// Node.js 读取文件时,成功回调和失败回调,是通过 error参数来区分
|
||
readFile('d:\\readme.text', function (err, data) {
|
||
if (error) {
|
||
console.log('文件读取失败');
|
||
} else {
|
||
console.log('文件读取成功');
|
||
}
|
||
});
|
||
|
||
// jQuery的 ajax 写法中,成功回调和失败回调,是通过两个回调函数来区分
|
||
$.ajax({
|
||
url: '/ajax.json',
|
||
success: function (response) {
|
||
console.log('文件读取成功');
|
||
},
|
||
error: function (err) {
|
||
console.log('文件读取失败');
|
||
},
|
||
});
|
||
```
|
||
|
||
我们可以看到,上面的回调函数的代码中,成功回调和失败回调,**参数的写法不一致**。在实战开发中,**封装异步函数的人和调用异步函数的人,往往不是同一个人**。甚至可能出现的极端的情况是,回调函数里需要传很多参数,**参数的顺序也不一致**,各有各的风格,每个人写得都不一样。因为这种回调参数的写法**不一致、不规范**的问题,所以需要单独记忆,导致在调用时需要小心翼翼,很容易出错。
|
||
|
||
### 小结
|
||
|
||
按照上面的分析,在 ES5 中处理异步任务时,产生的这两个问题,ES6 中的 Promise 就可以解决。当然, Promise 的强大功能,不止于此。我们去下一篇文章一探究竟。
|
||
|
||
## 赞赏作者
|
||
|
||
创作不易,你的赞赏和认可,是我更新的最大动力:
|
||
|
||
![](https://img.smyhvae.com/20220401_1800.jpg) |