什么是回调地狱,如何用Promise解决回调地狱,看完这篇你就明白了。
前言
回调地狱,其实简单来说就是异步回调函数的嵌套。我们先了解一点什么是回调函数和异步任务。
1.回调函数:
把一个函数当作参数传递,传递的是函数的定义并不会立即执行,而是在将来特定的时机再去调用,这个函数就叫做回调函数。在定时器setTimeout以及Ajax的请求时都会用到回调函数。
setTimeout(function(){ //这个function()就是回调函数,它只有在1秒后才会执行
console.log('执行了回调函数');
},1000)
2.异步任务:
与之相对应的概念是“同步任务”,同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。前一个任务是否执行完毕不影响下一个任务的执行。
一.什么是回调地狱(Callback Hell)
存在异步任务的代码,不能保证能按照顺序执行,如果我们需要代码顺序执行,要怎么写呢?
setTimeout(function () { //第一层
console.log(111);
setTimeout(function () { //第二程
console.log(222);
setTimeout(function () { //第三层
console.log(333);
}, 1000)
}, 2000)
}, 3000)
这种回调函数的层层嵌套,就叫做回调地狱。回调地狱会造成代码可复用性不强,可阅读性差,可维护性(迭代性差),扩展性差等等问题。
那该如何解决回调地狱呢?
二.解决方法
1. Promise语法
1.1 对Promise的理解
Promise,中文翻译过来就是'承诺',意思是在未来某一个时间点承诺返回数据给你。它是js中的一个原生对象,是一种异步编程的解决方案,可以替换掉传统的回调函数解决方案。
promise本身只是一个容器,真正异步的是它的两个回调resolve()和reject()
promise本质 不是控制 异步代码的执行顺序(无法控制) , 而是控制异步代码结果处理的顺序
-
promise对象有三个状态:pending(进行中),fulfilled(已成功),rejected(已失败)
-
如何改变promise的状态:
- resolve(value): 如果当前是 pending 就会变为 resolved
- reject(error): 如果当前是 pending 就会变为 rejected
- 抛出异常: 如果当前是 pending 就会变为 rejected
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
1.2 创建Promise实例
一般情况下都会使用new Promise()
来创建promise对象,但是也可以使用promise.resolve
和promise.reject
这两个方法:
- Promise.resolve
Promise.resolve(value)
的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:
Promise.resolve(11).then(function(value){
console.log(value); // 打印出11
});
- Promise.reject
Promise.reject
也是new Promise
的快捷形式,也创建一个promise对象。代码如下:
Promise.reject(new Error(“我错了!!”));
1.3 使用Promise解决地狱回调
1. promise创建时,里面的代码还是异步无序操作
2. promise的原理是,利用then方法将异步操作的结果,按照顺序执行,catch方法用来接收处理失败时相应的数据。
在上一个promise的then方法中,返回下一个promise对象
总结: 不要在创建promise的时候去处理异步操作结果,而应该通过 then() 方法来处理
//1.封装一个函数 : 根据文件名生成 文件读取的promise
function getPromise(fileName) {
let p = new Promise((resolve, reject) => {
//读文件
fs.readFile(`./data/${fileName}.txt`, 'utf-8', (err, data) => {
if (err == null) {
//成功
resolve(data);
} else {
//失败
reject(err);
}
});
});
return p;
};
//2.解决需求: 要先读a, 读完a后读b, 读完b后读c.
//开始读取a
getPromise('a').then((data)=>{
console.log(data);
//继续读取b
return getPromise('b');
}).then((data)=>{
console.log(data);
//继续读取c
return getPromise('c');
}).then((data)=>{
console.log(data);
}).catch((err)=>{
//以上三个异步操作,只要有任何一个出错,都会执行err
console.log(err);
});
1.4 Promise的实例方法和静态方法
实例方法
then
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved
时调用,第二个回调函数是Promise对象的状态变为rejected
时调用。其中第二个参数可以省略。 then
方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then
方法后面再调用另一个then方法。
catch
该方法相当于then
方法的第二个参数,指向reject
的回调函数。不过catch
方法还有一个作用,就是在执行resolve
回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch
方法中。
finally
finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally
方法关掉服务器。
server.listen(port)
.then(function () {
// ...
})
.finally(server.stop);
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
静态方法
all
all
方法可以完成并发任务, 它接收一个数组,数组的每一项都是一个promise
对象,返回一个Promise
实例。当数组中所有的promise
的状态都达到resolved
的时候,all
方法的状态就会变成resolved
,如果有一个状态变成了rejected
,那么all
方法的状态就会变成rejected
。
race
race
方法和all
一样,接受的参数是一个每项都是promise
的数组,但是与all
不同的是,当最先执行完的事件执行完之后,就直接返回该promise
对象的值。如果第一个promise
对象状态变成resolved
,那自身的状态变成了resolved
;反之第一个promise
变成rejected
,那自身状态就会变成rejected
。
any
它接收一个数组,数组的每一项都是一个promise
对象,该方法会返回一个新的 promise
,数组内的任意一个 promise
变成了resolved
状态,那么由该方法所返回的 promise
就会变成resolved
状态。如果数组内的 promise
状态都是rejected
,那么该方法所返回的 promise
就会变成rejected
状态,
resolve、reject
用来生成对应状态的Promise实例
Promise.all、Promise.race、Promise.any的区别
all: 成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
race: 哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
any: 返回最快的成功结果,如果全部失败就返回失败结果。
1.5 Promise的问题
Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担,并且Promise传递中间值⾮常麻烦
Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。
所以ES2017推出了新的语法async/await
来更好的解决异步问题。
2. async/await
2.1 什么是async/await
- 一句话概括: 它就是 Generator 函数的语法糖,也就是处理异步操作的另一种
高级写法
async语法如下:
-
函数前面使用
async
修饰 -
函数内部,promise操作使用
await
修饰
- await 后面是promise对象, 左侧的返回值就是这个promise对象的then方法中的结果
- await必须要写在async修饰的函数中,不能单独使用,否则程序会报错
2.2 用async/await语法解决地狱回调问题
const fs = require("fs");
/*
promise实例对象的catch方法 : 用于捕获异步操作的错误信息
*/
//1.封装一个函数 : 根据文件名生成 文件读取的promise
function getPromise(fileName) {
let p = new Promise((resolve, reject) => {
//读文件
fs.readFile(`./data/${fileName}.txt`, 'utf-8', (err, data) => {
if (err == null) {
//成功
resolve(data);
} else {
//失败
reject(err);
}
});
});
return p;
};
//2.解决需求: 要先读a, 读完a后读b, 读完b后读c.
// async和await异步函数 : 这两个关键字只能用于函数, 所以用的时候一定要放在函数里面用
/*
async关键字: 修饰函数。 表示这个函数内部有异步操作。
await关键字: 等待异步执行完毕。
(1)await只能用于被async修饰的函数中。
只有当await后面的异步操作执行完毕后,才会继续执行后面代码
(2)await 后面 只能是promise对象
*/
const readFile = async () => {
let data1 = await getPromise('a')
console.log(data1)
let data2 = await getPromise('b')
console.log(data2)
}
readFile()
2.3 async语法的异常捕获
async
函数内部的异常可以通过 .catch()
或者 try
/catch
来捕获,区别是
-
try/catch 能捕获所有异常,try语句抛出错误后会执行catch语句,try语句内后面的内容不会执行
-
catch()只能捕获异步方法中reject错误,并且catch语句之后的语句会继续执行
async函数错误捕获,以登录功能为例
async function getCatch () {
await new Promise(function (resolve, reject) {
reject(new Error('登录失败'))
}).catch(error => {
console.log(error) // .catch()能捕获到错误信息
})
console.log('登录成功') // 但是成功信息也会执行
}
async function getCatch () {
try {
await new Promise(function (resolve, reject) {
reject(new Error('登录失败'))
})
console.log('登录成功') // try抛出错误之后,就不会执行这条语句
} catch (error) {
console.log(error) // catch语句能捕获到错误信息
}
}
三.总结
当我们写代码遇到异步回调时,我们想让异步代码按照我们想要的顺序执行,如果按照传统的嵌套方式,就会出现回调地狱,这样的代码不利于维护。我们可以通过Promise对象进行链式编程来解决,这样尽管可以解决问题,但是ES7给我们提供了更加舒适的async/await语法糖。
希望以上文章能够给读者一些帮助。
转载自:https://juejin.cn/post/7108187709076111367