promise 和 async 函数解决“回调地狱”
- 使用回调函数的痛
- 使用 Promise 解决 callback hell
- 什么是 promise,怎么使用promise
- 使用 promise 解决 callback hell
- 使用 async 解决 callback hell
- 什么是 async,怎么使用async
- 使用 async 解决 callback hell
Node.js提供的非阻塞IO模型允许我们利用回调函数的方式处理IO操作,但是当需要连续的IO操作时,你的回调函数会出现多重嵌套,最后陷入“回调地狱”(callback hell)。
使用回调函数的痛 举个栗子:
现在有三个文件:a.txt,b.txt,c.txt,内容如下。我们要做的是读取 a 文件拿到 b 文件的文件名,再读取 b 文件拿到 c 文件的文件名,最后读取 c 文件,打印 c 文件中的内容。
**项目目录**
- node_test
a.txt// content: b.txt
b.txt// content: c.tx
c.txt// content: finish
readFileData.js
readFileData.js 中的内容:
const fs = require('fs');
// 读取a文件内容
fs.readFile('a.txt', {flag: 'r', encoding: 'utf-8'}, (err, aData) => {
if (err) {
console.log(err);
} else {
// 读取b文件内容
fs.readFile(aData, 'utf-8', (err, bData) => {
if (err) {
console.log(err);
} else {
// 读取c文件内容
fs.readFile(bData, 'utf-8', (err, cData) => {
if (err) {
console.log(err);
} else {
// 打印c文件内容
console.log(cData);
}
})
}
})
}
})out:
finish
可以看的出来,才三层嵌套就已经招架不住了,晕。
解决这个问题可以使用 promise 或者 async。
使用 Promise 解决 callback hell 什么是 promise,怎么使用promise
promise是一个对象,它通常代表一个在未来可能完成的异步操作。
- 利用 Promise 构造函数构建 promise 对象
const p = new Promise((resolve, reject) => { // promise函数体if (...) { resolve(value); } else { reject(new Error('错误说明')); } });
- Promise 构造函数需要传入一个参数,这个参数是一个函数,假设它叫 promiseFun。
- promiseFun 需要两个参数,习惯把第一个参数叫做 resolve,第二个参数叫做 reject。
// 参数resolve和reject也是一个函数,原型定义如下: function resolve(value) {...} function reject(reason) {...}// reject函数的参数可以是一个字符串,但是建议使用 new Error(),这样显得比较规范
- promiseFun 不需要返回值,它的执行结果只有两种,成功 or 失败,把成功的结果传入 resolve 函数,失败的结果传入 reject 函数。
3.1. Promise其实有三种状态:pending、 resolve、 reject
3.2. 如果promise函数体中既没有调用resolve函数,又没有调用reject函数,prmise对象就会是pending状态
const p = new Promise((resolve, reject) => { // promise函数体 console.log('pending状态'); }); p.then(...).then(...).catch()// pending状态下,p之后的代码都不会执行,也就是跳出了promise的链式调用
- resolve 和 reject 这两个函数谁先调用,promise对象的状态就会锁定谁,另外一个就算被调用也没有任何效果。
resolve(value); reject(err); 这样调用之后,promise对象的状态就会锁定在 resolve(成功),后面的 reject(err) 没有任何意义。
- 在 promiseFun 函数体中,我们可以看到 resolve 和 reject 是直接调用的,并没有定义,它们是在未来定义的,在这里提前使用。
- 定义resolve和reject函数体
- Promise 的内置函数 then 和 catch 就是真正用来定义 resolve 和 reject 函数体的。
p .then(function(value) { // 这里是 resolve 函数的具体定义 }) .then(function(value) { // 这里是 resolve 函数的具体定义 }) .catch(function(reason) { // 这里是 reject 函数的具体定义 })
- promise.then() 函数的返回值是一个Promise对象,这是为了构造then()函数链(即后面可以继续调用then函数)。
- promise.catch() 函数的返回值也是一个Promise对象(尽管我们通常不会使用这个对象)。
3.1. catch 的位置放在最后,链式中任何一个环节出问题,都会被catch到,而且这个环节之后的代码不会再执行。
【Node|promise 和 async 函数解决“回调地狱”】3.2. catch 的位置在某个then()函数后面,catch函数前的任何一个环节出问题,都会被catch到,而且这个环节之后的then()函数会继续执行。
,p .then(function(value) { return p2// p2 是Promise对象 }) .catch(function(reason) { // 如果 p2 的状态锁定的是reject(失败),就会被catch到 return 2 }) .then(function(value) { // 处理完catch函数之后会继续执行这个then()函数 // 参数value的值取决于catch函数的参数函数的返回值,参考下面的“resolve 函数的两种返回值” // 如果catch函数没有返回值,则 value 的值为 undefined })
- resolve 函数的两种返回值
- resolve 返回值是:数字、字符串、对象等,会作为下个 then(resolve) 函数中参数函数 resolve 的参数。
p.then(function(value) { return 2 }) .then(function(value) { console.log(value) }) .catch(function(reason) { // 这里是 reject 函数的具体定义 })out: 2
- resolve 返回值是promise对象,那么 p.then() 方法的返回值就是这个promise对象,再用这个promise对象继续调用 then() 方法。
p.then(function(value) { const p2 = new Promise((resolve, reject) => { // promise函数体if (...) { resolve(value); } else { reject(err); } }) return p2// p2 是Promise对象 }) .then(function(value) {// 相当于 p2.then() //这里是 p2 对象的 resolve 函数的具体定义 }) .catch(function(reason) { // 这里是 reject 函数的具体定义 })
- 如何终止或跳出promise链式调用
- 手动抛出异常,前提是catch函数放在最后。
1.1. 这样做无法判断是程序错误跳出还是手动跳出。
p.then(function(value) { return 1; }) .then(function(value) { console.log(value); throw new Error('手动抛出异常'); }) .then(function(value) { console.log(value); return 2; }) .catch(function(err) { console.log(err); }); out: 1 手动抛出异常
- then()函数中返回 Promise.reject(err),前提是catch函数放在最后。
2.1. 有时候这样并不能确定是程序错误被catch到,还是手动跳出被catch到。
2.2. 为了解决上述问题,我们可以在catch函数中判断错误类型是否是 Error 对象来进行区别(这就是为什么前面建议reject函数的参数使用 new Error())。
p.then(function(value) { return 1; }) .then(function(value) { console.log(value); return Promise.reject('利用reject主动跳出'); }) .then(function(value) { console.log(value); }) .catch(function(err) { if (err instanceof Error) { console.log('程序错误退出'); } else { console.log('主动跳出'); } }); out: 1 主动跳出
- 通过在then()函数中返回 promise 的 pending 状态来终止链式调用,catch函数的位置不在最后的情况。
3.1. 因为promise一直处于pending状态,有可能会出现内存泄漏的现象,不建议大量使用这种方式。
p.then(function(value) { return 1; }) .then(function(value) { console.log(value); // 返回一个新的promise对象,么有resolve和reject,一直处于pending状态,所以链式调用在这里就被中断了 return new Promise(() => {console.log('利用pending主动跳出')}); }) .catch(function(err) { if (err instanceof Error) { console.log('程序错误退出'); } else { console.log('主动跳出'); } }) .then(function(value) { console.log(value); }); out: 1 利用pending主动跳出
console.log('step-1');
function readFileData(fileName) {console.log('promise-1');
// 构建promise对象并返回
return new Promise((resolve, reject) => {console.log('promise-2');
// 读取文件
fs.readFile(fileName, 'utf-8', (err, data) => {console.log('readFile', fileName);
if (err) {reject(err);
// 读取失败则把错误信息传入reject函数,等待catch函数处理} else {resolve(data);
// 读取成功则把数据传入resolve函数,等待then函数处理console.log(fileName, '读取成功!');
}
})
});
}console.log('step-2');
readFileData('a.txt') // 返回promise对象,异步读取a文件内容.then(aData => {// 定义resolve函数,参数aData是a文件内容console.log('a文件内容:', aData);
return readFileData(aData);
// 返回promise对象,异步读取b文件内容}).then(bData => {// 定义resolve函数,参数bData 是b文件内容console.log('b文件内容:', bData);
return readFileData(bData);
// 返回promise对象,异步读取c文件内容}).then(cData => {// 定义resolve函数,参数cData 是c文件内容
console.log('c文件内容:', cData);
}).catch(err => {// 定义reject函数,参数err是读取文件是的错误信息(如果读取失败的话)console.log(err);
});
console.log('end');
out:
step-1
step-2
promise-1
promise-2
end
readFile a.txt
a.txt 读取成功!
a文件内容: b.txt
promise-1
promise-2
readFile b.txt
b.txt 读取成功!
b文件内容: c.txt
promise-1
promise-2
readFile c.txt
c.txt 读取成功!
c文件内容: finish
注意:
- 从out的前 5 句可以看出,Promise的执行仍然是异步方式的,并没有改变成同步执行模式,只不过让代码写起来读起来像是同步执行一样。
- 从out的第 3,4 句可以看出,Promise函数体是在Promise对象创建的时候就被执行了。
- 从out的第 6,7, 8 句可以看出:
3.1:resolve或者reject函数已经在Promise函数体内被调用了,而此时resolve和reject的值并没有被定义了,怎么办?其实这就是Promise机制实现的功能,可是先调用一个未定义的函数,等将来函数被定义的时候(then())再真正执行函数体。
3.2:then/catch函数体并不是在then/catch被调用的时候执行的,而是在后面的某一个异步时间点被执行,对他们的执行是在稍后以异步事件的方式回调的,具体的回调时间是不确定的。这也是Promise机制实现的功能。
使用 promise 虽然能够解决 callback hell,但并不是最好的方法,使用的时候总是一堆 then()。
es7 给我们提供了更好的方法,使用 async 和 await 。
- 使用 async 声明异步函数
async function fn() {} // 或者 async () => {}
- async 函数的返回值是 promise 对象
1.1. 从下面的例子可以看到,如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装
async function asyncFun() { return 'promise'; // 等同于 return Promise.resolve('promise'); // 等同于 return new Promise((resolve, reject) => { resolve('promise') }) }
- 怎么拿到 async 函数中返回的字符串呢?
2.1. 因为async函数的返回值是promise对象( Promise.resolve(value) ),所以要通过then()函数来获取 value 值
2.2. 注意调用then()函数的是async函数的返回值 asyncFun(),必须加括号
asyncFun().then(value => { console.log(value); })out: promise
- 在 async 函数中使用 await
- await 后面跟 promise 对象,必须等到promise对象有返回值的时候,代码才会继续执行下去
1.1. 可以看出来,使用 await 之后,不需要调用 then() 函数,可以直接得到 Promise.resolve(value) 的 value 值
function setDelay(second) { return new Promise((resolve, reject) => { // 传入的不是数值型数据就会调用 reject if (typeof second != 'number') { reject('输入的不是number类型的值!') }setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000); }); }async function asyncFun() { try{ // 会在这里等待 1 秒,拿到返回值后再继续执行下面的代码 const result01 = await setDelay(1); // 会在这里等待 2 秒,拿到返回值后再继续执行下面的代码 const result02 = await setDelay(2); console.log(result01); console.log(result02); } catch (e) { console.log(e); } }asyncFun(); out: 延迟了1秒输出 延迟了2秒输出
注意:
- await 必须在 async 函数中使用,在 async 函数外使用会报错。
SyntaxError: await is only valid in async function
- await 必须在 async 函数中使用,在 async 函数外使用会报错。
- await 后面跟其他值,不会阻塞后面的代码,相当于同步
2.1. 从下面的例子可以看出来,并没有因为使用 await 就延时两秒才去执行 console.log(‘end’); 这句代码,也没有阻塞 then() 函数的调用
async function asyncFun() { // await 后面是 setTimeout,是否会等待两秒再执行后面的代码 const result = await setTimeout(() => { console.log('延时了两秒') }, 2000); console.log('end'); // 返回 promise 对象 return result; }asyncFun().then(value => { console.log(value); }); out: end Timeout { _idleTimeout: 2000, _idlePrev: [TimersList], _idleNext: [TimersList], _idleStart: 23, _onTimeout: [Function], _timerArgs: undefined, _repeat: null, _destroyed: false, [Symbol(refed)]: true, [Symbol(asyncId)]: 2, [Symbol(triggerId)]: 1 } 延时了两秒
- 处理 async 函数中的异常
3.1. 使用 try … catch 捕获异常,这种方法在 async 函数中使用
function setDelay(second) { return new Promise((resolve, reject) => { // 传入的不是数值型数据就会调用 reject if (typeof second != 'number') { reject('输入的不是number类型的值!') }setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000); }); }async function asyncFun() { try{ // 这里会产生异常 const result01 = await setDelay('1'); // 这里的代码不会继续执行 const result02 = await setDelay(2); console.log(result01); console.log(result02); } catch (e) { console.log(e); } }asyncFun(); out: 输入的不是number类型的值!
3.2. 使用 Promise.catch() 捕获异常,这种方法在 async 函数外使用
function setDelay(second) { return new Promise((resolve, reject) => { // 传入的不是数值型数据就会调用 reject if (typeof second != 'number') { reject('输入的不是number类型的值!') }setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000); }); }async function asyncFun() { try{ // 这里会产生异常 const result01 = await setDelay('1'); // 这里的代码不会继续执行 const result02 = await setDelay(2); console.log(result01); console.log(result02); } catch (e) { console.log(e); } }asyncFun().catch(err => { console.log(err); }); out: 输入的不是number类型的值!
3.4. 上面两种方法都会中断程序的执行。在 async 函数内使用 Promise.catch() 捕获异常,程序不会终止(类似于Promise中把catch函数提前)
function setDelay(second) { return new Promise((resolve, reject) => { // 传入的不是数值型数据就会调用 reject if (typeof second != 'number') { reject('输入的不是number类型的值!') }setTimeout(() => resolve(`延迟了${second}秒输出`), second * 1000); }); }function catchErr(promise) { // 处理promise对象,返回数据或错误 return promise.then(value => { return [null, value]; }) .catch(err => { return [err]; }) }async function asyncFun() { // 这里会产生异常 [err01, result01] = await catchErr(setDelay('1')); // 判断是否有异常 if (err01) { // 想继续执行就不抛出错误 console.log('出现错误,但是想让下面的代码继续执行!'); } else { console.log(result01); }// 这里的代码会继续执行 [err02, result02] = await catchErr(setDelay('2')); // 判断是否有异常 if (err02) { // 不想继续执行就抛出错误 throw new Error('出现错误,而且不想继续执行下面的代码!'); } else { console.log(result02); }// 这里的不会再执行 [err03, result03] = await catchErr(setDelay(3)); // 判断是否有异常 if (err03) { // 不想继续执行就抛出错误 throw new Error('出现错误,而且不想继续执行下面的代码!'); } else { console.log(result03); } }asyncFun(); out: 出现错误,但是想让下面的代码继续执行! (node:90200) UnhandledPromiseRejectionWarning: Error: 出现错误,而且不想继续执行下面的代码! ...
console.log('step-1');
function readFileData(fileName) {
// 构建promise对象并返回
return new Promise((resolve, reject) => {// 读取文件
fs.readFile(fileName, 'utf-8', (err, data) => {console.log('readFile', fileName);
if (err) {reject(err);
// 读取失败则把错误信息传入reject函数,等待catch函数处理} else {resolve(data);
// 读取成功则把数据传入resolve函数,等待then函数处理console.log(fileName, '读取成功!');
}
})
});
}console.log('step-2');
async function asyncReadFile() {try{const aData = https://www.it610.com/article/await readFileData('a.txt');
// 读取a文件内容
console.log('读取a文件内容结束');
const bDta = await readFileData(aData);
// 读取b文件内容
console.log('读取b文件内容结束');
const cDtae = await readFileData(bDta);
// 读取c文件内容
console.log('读取c文件内容结束');
console.log(cDtae);
} catch(err) {console.log(err);
}
}asyncReadFile();
console.log('end');
out:
step-1
step-2
end
readFile a.txt
a.txt 读取成功!
读取a文件内容结束
readFile b.txt
b.txt 读取成功!
读取b文件内容结束
readFile c.txt
c.txt 读取成功!
读取c文件内容结束
finish
注意:
从out的前 5 句可以看出:
- 在 asyncReadFile();
之前的代码都是在定义函数,所以首先打印 step-1,step-2。
- 在执行到 asyncReadFile();
这一句时,会立即执行 asyncReadFile 函数里面的代码。
- 在 asyncReadFile 函数里,首先执行 const aData = https://www.it610.com/article/await readFileData(‘a.txt’);
这一句。
- 这时会调用 readFileData(‘a.txt’) 函数,创建promise对象(前面说过创建promise对象会立即执行promise函数体)。
- 在 readFileData 函数中,当执行到 fs.readFile(fileName, ‘utf-8’, (err, data) … 时,因为 readFile 函数是异步的,这时主程序不会阻塞在这里,而是回到 console.log(‘end’);
这一句继续执行,打印 end。
- 执行完 console.log(‘end’);
之后,程序又回到 asyncReadFile 中,这时 readFile 函数才会执行。
- async 函数的执行仍然是异步方式的,并没有改变成同步执行模式,只不过让代码写起来读起来像是同步执行一样。
- 在 async 函数中遇到异步任务时,程序会回到 async 函数外继续执行后面的同步代码。同步代码执行完,再回到async内部,继续执行await后面的代码
- async 函数中的代码是同步的,必须等 await 拿到返回值,才能继续执行下面的代码。
推荐阅读
- vue.js|vue中使用axios封装成request使用
- JavaScript|JavaScript: BOM对象 和 DOM 对象的增删改查
- 前端|web前端dya07--ES6高级语法的转化&render&vue与webpack&export
- JS/JavaScript|JS/JavaScript CRC8多项式 16进制
- JS|VUE学习笔记[30-46]
- 腾讯TEG实习|腾讯实习——Vue解决跨域请求
- 地图|高德地图清除指定覆盖物 自定义覆盖物样式(完整dome)
- Pyecharts|Pyecharts 猎聘招聘数据可视化
- flex|C语言-使用goto语句从循环中跳出
- ES6语法