如何避免回调地狱

986 阅读6分钟

问题

平常写代码的时候,同等条件下,异步代码是随机执行的,当需要一些异步操作按顺序执行,难以避免异步回调,层层嵌套,产生回调地狱。

  • 避免回调地狱的方式
  1. Promise
  2. async/await
  3. generator
  4. 事件发布/监听模式
  • 来看一下回调嵌套代码
//需求: 依次读取 文件  a.txt , b.txt , c.txt 这三个文件内容
const fs = require('fs');

//(1)能直接按照顺序写吗?  : 不能,因为异步操作 是无序的
fs.readFile("./data/a.txt", 'utf-8', (err, data) => {
    if(err){
        console.log(err);
    }else{
        console.log(data);
    };
});

fs.readFile("./data/b.txt", 'utf-8', (err, data) => {
    if(err){
        console.log(err);
    }else{
        console.log(data);
    };
});

fs.readFile("./data/c.txt", 'utf-8', (err, data) => {
    if(err){
        console.log(err);
    }else{
        console.log(data);
    };
});

//(2)解决方案 : 在回调函数中嵌套执行
//弊端 : 形成回调地狱(异步回调 层层嵌套,非常麻烦且不便于维护)
//读取文件A
fs.readFile("./data/a.txt", 'utf-8', (err, data) => {
    if(err){
        console.log(err);
    }else{
        console.log(data);
        //A读取成功之后开始读取B
        fs.readFile("./data/b.txt", 'utf-8', (err, data) => {
            if(err){
                console.log(err);
            }else{
                console.log(data);
                //B读取成功之后开始读取C
                fs.readFile("./data/c.txt", 'utf-8', (err, data) => {
                    if(err){
                        console.log(err);
                    }else{
                        console.log(data);
                    }
                });
            }
        });
    }
});

  • 可以看到,每增加一个异步请求,就会多加一层回调函数嵌套,这样下去可读性会越来越差非常麻烦,同时也不利于代码的维护,当回调太多了就会陷入回调地狱,接下来主要会讲Promise和async/await来解决这个问题。

Promise

Promise的基本使用

  • 1.实例化promise对象
    • 作用:将异步代码放入promise中
      • resolve:异步操作 成功状态
      • reject:异步操作 失败状态
    • new Promise ((resolve,reject)=>{ 你的异步操作 })
  • 2.调用then()方法处理异步操作结果
    • promise对象.then((data)=>{ 处理成功数据 },(err)=>{ 处理失败的信息 })
  • 看一下实际代码
/* 
1.Promise是一个构造函数 , 返回一个Promise对象
2.使用流程
    (1)实例化Promise对象 : 将异步操作放入Promise中
    (2)调用then() 方法: 处理异步操作结果
*/

const fs = require('fs');
/** 1. 实例化Promise
* @description:实例化Promise
* @param {Function}  (resolve:成功处理函数,reject:失败处理函数)=>{ 异步操作代码 }
* @return: Promise对象
*/
 const p1 = new Promise((resolve,reject)=>{
     //读文件
     fs.readFile('./data/a.txt','utf8',(err,data)=>{
        if(err == null){ 
            /* 
            (1)异步操作成功,则执行 resolvce()
            (2)resolve会把把promise对象的状态从 pending进行中 改为 fulfilled成功
            (3)该方法本质是调用 then() 中的第一个方法
            */
            resolve(data);
        }else {
            /* 
            (1)异步操作失败,则执行 reject()
            (2)reject会把把promise对象的状态从 pending进行中 改为 rejected失败
            (3)该方法本质是调用 then() 中的第二个方法
            */
            reject(err); 
        }
     });
 });

 /* 2. p1.then() : 处理异步操作结果 */
 p1.then((data)=>{
    //成功了,打印文件数据
    console.log(data);
 },(err)=>{
    //失败了,打印错误信息
    console.log(err);
 });

Promise特点

promise本质不是控制异步代码的执行顺序(无法控制),而是控制异步代码结果处理的顺序 promise本身只是一个容器,真正异步的是它的两个回调resolve()和reject()

  • 1.promise对象有三个状态.

    • a. pending(进行中)
    • b. fulfilled(已成功)
    • c. rejected(已失败)
  • 2.Promise对象的状态改变, 只有两种可能:

    • a. 从pending变为fulfilled
      • 此时应该执行 resolve();
    • b. 从pending变为rejected。
      • 此时应该执行 reject();
  • 3.promise在创建对象的时候,里面的代码会立即执行.

    • a. promise创建时,里面的代码还是异步无序操作
    • b. promise的原理是,利用then方法将异步操作的结果 按照顺序执行
    • 总结: 不要在创建promise的时候去处理异步操作结果,而应该通过 then() 方法来处理
  • 4.promise解决回调地狱原理 :

    • 在then方法中返回一个promise对象
    • 在上一个promise的then方法中,返回下一个promise
  • 5.结语 : promise本质 不是控制异步代码的执行顺序(无法控制) , 而是控制异步代码结果处理的顺序

const fs = require('fs');

//(1) 创建三个异步操作  promise

//读取文件A
const p1 = new Promise((resolve,reject)=>{
    //读文件
    fs.readFile('./data/a.txt','utf8',(err,data)=>{
       if(err == null){ 
            //成功
           resolve(data);
       }else {
           //失败
           reject(err); 
       }
    });
});

//读取文件B
const p2 = new Promise((resolve,reject)=>{
    //读文件
    fs.readFile('./data/b.txt','utf8',(err,data)=>{
       if(err == null){ 
            //成功
           resolve(data);
       }else {
           //失败
           reject(err); 
       }
    });
});

//读取文件C
const p3 = new Promise((resolve,reject)=>{
    //读文件
    fs.readFile('./data/c.txt','utf8',(err,data)=>{
       if(err == null){ 
            //成功
           resolve(data);
       }else {
           //失败
           reject(err); 
       }
    });
});

// (2)按照顺序处理异步操作结果
p1.then((data)=>{//第一个异步结果
    console.log(data);
    return p2;//返回下一个promise
}).then((data)=>{ // 第二个异步结果  (由于p1.then方法返回的是p2,而p2也有自己的then,所以可以继续调用p2的then方法)
    console.log(data);
    return p3;//返回下一个promise
}).then((data)=>{ // 第三个异步结果  
    console.log(data);
});

Promise解决回调地狱封装

const fs = require('fs');


//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);
});//异步回调队列结束

Promise对象的catch方法

  • catch用于扑捉异常信息
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.

//开始读取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);
});

async与await

异步函数async相当于是promise语法的 “高级写法”
ES2017中引入的更为高级的异步处理机制,`async`函数,可以让异步的处理变的更加便捷
  • 一句话概括:async函数相当于是promise异步函数的另一种高级写法
    • promise虽然解决了异步回调地狱(回调函数层次嵌套)的问题,但是写起来的时候仍需要嵌套(链式语法嵌套,需要上一个promise对象的then方法中返回下一个promise)
  • 1.传统的promise异步函数(模拟依次异步读取文件a,b,c)
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.

//开始读取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);
});
  • 使用async异步函数
  • async语法如下
    • (1) 函数前面使用async修饰
    • (2)函数内部,promise操作使用await修饰
      • await后面是promise对象,左侧的返回值就是这个promise对象的then方法中的结果
      • await必须要写在async修饰的函数中,不能单独使用,否则程序报错
        • async函数内部的异常需要通过try,catch来铺获
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)

    //async异步函数的错误信息要用try-catch来捕捉
    try {
        let data3 = await getPromise('c')
        console.log(data3)
    } catch (err) {
        console.log(err)

    }
}

readFile()