Node|promise 和 async 函数解决“回调地狱”


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('错误说明')); } });

  1. Promise 构造函数需要传入一个参数,这个参数是一个函数,假设它叫 promiseFun。
  2. promiseFun 需要两个参数,习惯把第一个参数叫做 resolve,第二个参数叫做 reject。
    // 参数resolve和reject也是一个函数,原型定义如下: function resolve(value) {...} function reject(reason) {...}// reject函数的参数可以是一个字符串,但是建议使用 new Error(),这样显得比较规范

  3. 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的链式调用

  4. resolve 和 reject 这两个函数谁先调用,promise对象的状态就会锁定谁,另外一个就算被调用也没有任何效果。
    resolve(value); reject(err); 这样调用之后,promise对象的状态就会锁定在 resolve(成功),后面的 reject(err) 没有任何意义。

  5. 在 promiseFun 函数体中,我们可以看到 resolve 和 reject 是直接调用的,并没有定义,它们是在未来定义的,在这里提前使用。
  • 定义resolve和reject函数体
  1. Promise 的内置函数 then 和 catch 就是真正用来定义 resolve 和 reject 函数体的。
    p .then(function(value) { // 这里是 resolve 函数的具体定义 }) .then(function(value) { // 这里是 resolve 函数的具体定义 }) .catch(function(reason) { // 这里是 reject 函数的具体定义 })

  2. promise.then() 函数的返回值是一个Promise对象,这是为了构造then()函数链(即后面可以继续调用then函数)。
  3. 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 函数的两种返回值
  1. resolve 返回值是:数字、字符串、对象等,会作为下个 then(resolve) 函数中参数函数 resolve 的参数。
    p.then(function(value) { return 2 }) .then(function(value) { console.log(value) }) .catch(function(reason) { // 这里是 reject 函数的具体定义 })out: 2

  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链式调用
  1. 手动抛出异常,前提是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 手动抛出异常

  2. 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 主动跳出

  3. 通过在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主动跳出

使用 promise 解决 callback hell
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

注意:
  1. 从out的前 5 句可以看出,Promise的执行仍然是异步方式的,并没有改变成同步执行模式,只不过让代码写起来读起来像是同步执行一样。
  2. 从out的第 3,4 句可以看出,Promise函数体是在Promise对象创建的时候就被执行了。
  3. 从out的第 6,7, 8 句可以看出:
    3.1:resolve或者reject函数已经在Promise函数体内被调用了,而此时resolve和reject的值并没有被定义了,怎么办?其实这就是Promise机制实现的功能,可是先调用一个未定义的函数,等将来函数被定义的时候(then())再真正执行函数体。
    3.2:then/catch函数体并不是在then/catch被调用的时候执行的,而是在后面的某一个异步时间点被执行,对他们的执行是在稍后以异步事件的方式回调的,具体的回调时间是不确定的。这也是Promise机制实现的功能。
使用 async 解决 callback hell 什么是 async,怎么使用async
使用 promise 虽然能够解决 callback hell,但并不是最好的方法,使用的时候总是一堆 then()。
es7 给我们提供了更好的方法,使用 async 和 await 。
  • 使用 async 声明异步函数
    async function fn() {} // 或者 async () => {}

  1. async 函数的返回值是 promise 对象
    1.1. 从下面的例子可以看到,如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装
    async function asyncFun() { return 'promise'; // 等同于 return Promise.resolve('promise'); // 等同于 return new Promise((resolve, reject) => { resolve('promise') }) }

  2. 怎么拿到 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
  1. 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秒输出

    注意:
    1. await 必须在 async 函数中使用,在 async 函数外使用会报错。
      SyntaxError: await is only valid in async function
  2. 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 } 延时了两秒

  3. 处理 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: 出现错误,而且不想继续执行下面的代码! ...

使用 async 解决 callback hell
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 句可以看出:
  1. 在 asyncReadFile(); 之前的代码都是在定义函数,所以首先打印 step-1,step-2。
  2. 在执行到 asyncReadFile(); 这一句时,会立即执行 asyncReadFile 函数里面的代码。
  3. 在 asyncReadFile 函数里,首先执行 const aData = https://www.it610.com/article/await readFileData(‘a.txt’); 这一句。
  4. 这时会调用 readFileData(‘a.txt’) 函数,创建promise对象(前面说过创建promise对象会立即执行promise函数体)。
  5. 在 readFileData 函数中,当执行到 fs.readFile(fileName, ‘utf-8’, (err, data) … 时,因为 readFile 函数是异步的,这时主程序不会阻塞在这里,而是回到 console.log(‘end’); 这一句继续执行,打印 end。
  6. 执行完 console.log(‘end’); 之后,程序又回到 asyncReadFile 中,这时 readFile 函数才会执行。
总结:
  1. async 函数的执行仍然是异步方式的,并没有改变成同步执行模式,只不过让代码写起来读起来像是同步执行一样。
  2. 在 async 函数中遇到异步任务时,程序会回到 async 函数外继续执行后面的同步代码。同步代码执行完,再回到async内部,继续执行await后面的代码
  3. async 函数中的代码是同步的,必须等 await 拿到返回值,才能继续执行下面的代码。

    推荐阅读