6

ES6 Promise详解 - 公众号-web前端进阶

 2 years ago
source link: https://www.cnblogs.com/zdsdididi/p/16599344.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

ES6 Promise详解

本文主要是对Promise本身的用法做一个全面解析而非它的原理实现,如果你对Promise的用法还不是很熟悉或者想加深你对Promise的理解,我相信这篇文章一定会帮到你。

首先让我们先了解一下JavaScript为什么会引入Promise

让我们先看这样一段代码,JQuery中ajax请求:

  $.ajax({
      url: "url1",
      data: {},
      success(res1) {
        //获取到第一个数据
        console.log(res1);
        //根据第一个数去去获取第二个数据
        $.ajax({
          url: "url2",
          data: {
            query: res1.xxx,
          },
          success(res2) {
            //获取到第二个数据
            console.log(res2);
            //根据第二个数去去获取第三个数据
            $.ajax({
              url: "url3",
              data: {
                query: res2.xxx,
              },
              success(res3) {
                //获取到第三个数据
                console.log(res3);
                //...
              },
            });
          },
        });
      },
      error(err) {
        console.log(err);
      },
    });

我们会发现这些回调一层又一层,这就被称为回调地狱(callback hell),尤其业务逻辑复杂的时候这些回调就会变得难以维护。于是Promise就出现了。我们再看一个使用promise封装的axios请求:

 axios
      .get(url1, {})
      .then((res1) => {
        //获取到第一个数据
        console.log(res1);
        //根据第一个数去去获取第二个数据
        return axios.get(url2, { query: res1.xxx });
      })
      .then((res2) => {
        //获取到第一个数据
        console.log(res2);
        //根据第二个数去去获取第三个数据
        return axios.get(url3, { query: res2.xxx });
      })
      .then((res3) => {
        //获取到第三个数据
        console.log(res3);
        //...
      })
      .catch((err) => {
        console.log(err);
      });

通过对比我们会发现Promise解决了传统的回调函数的回调地狱问题,使得业务逻辑显得更加清晰了。接下来我们就开始正式介绍Promise了。

Promise是现代异步编程的基础,在Promise返回给我们的时候操作其实还没有完成,但Promise对象可以让我们操作最终完成时对其进行处理,无论成功还是失败。

Promise的返回有三种状态分别是等待(pending), 成功(fulfilled),拒绝(rejected),我们看以下示例(后续我们将用延时器setTimeout来代表我们的异步操作)

 const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      },1000);
    });
    console.log(promise1);

此时我们可以看到我们获取的Promise是pending(等待的状态)。

1658637004450.jpg

同样当我们一秒钟过后再去获取Promise

 const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(1);
      }, 1000);
    });
    setTimeout(() => {
      console.log(promise1);
    }, 1000);

它得到的就是成功(fulfilled)状态

1658637292640.jpg

然后我们将resolve换成reject

const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(1);
      }, 1000);
    });
    setTimeout(() => {
      console.log(promise1);
    }, 1000);

它得到的便是拒绝(rejected)状态,同时给你抛出了一个错误

1658637478041.jpg

Promise构造函数只有一个函数作为参数,这个函数会在一个Promise被实例化出来后会被立即执行

 new Promise((resolve, reject) => {
      console.log(1);
    });
    console.log(2);

此时输出的结果是:1 2

Promise接收的函数有两个参数,分别是resolve和reject,其中resolve代表一切正常的时候所调用的函数,reject则代表我们程序异常的时候所调用的函数。resolve函数传入的参数用于向下一个then传递一个值,而reject函数传入的参数则会被.catch捕捉。而Promise.finally则是在Promise状态完成后触发的一个回调,即无论是resolve还是reject都会触发

    //成功示例
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("成功的值");
      });
    })
      .then((res) => {
        console.log(res); //成功的值
      })
      .catch((err) => {
        //不会触发
        console.log(err);
      })
      .finally(() => {
        console.log("end"); //end
      });

    //失败示例
    new Promise((resolve, reject) => {
      setTimeout(() => {
        reject("失败的的值");
      });
    })
      .then((res) => {
        //不会触发
        console.log(res);
      })
      .catch((err) => {
        console.log(err); ////失败的值
      })
      .finally(() => {
        console.log("end"); //end
      });

以上便是Promise的基本使用,但是只掌握它的基本使用可不行,我们还需要对其更深入的研究

当我们使用Promise的时候,只要我们在.then的回调函数中返回一个成功状态(resolve)的Promise,则在下一个.then的回调函数中便可获取到这个成功函数(resolve)的参数,基于这个特性便有了Promise的链式调用。

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
        resolve("成功的值1");
    })
      .then((res) => {
        console.log(res); //成功的值1
        return new Promise((resolve, reject) => {
        //这里一般会有一个网络请求或其它异步操作
          resolve("成功的值2");
        });
      })
      .then((res) => {
        console.log(res); //成功的值2
        return new Promise((resolve, reject) => {
          //这里一般会有一个网络请求或其它异步操作
          resolve("成功的值3");
        });
      })
      .then((res) => {
        console.log(res); //成功的值3
        //以此类推...
      });

我们可以对其进行简写,比如

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      resolve("成功的值");
    });

可以简写为

Promise.resolve('成功的值')

所以我们的链式调用可以简写为

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      resolve("成功的值1");
    })
      .then((res) => {
        console.log(res); //成功的值1
        return Promise.resolve("成功的值2");
      })
      .then((res) => {
        console.log(res); //成功的值2
        return Promise.resolve("成功的值3");
      })
      .then((res) => {
        console.log(res); //成功的值3
        //以此类推...
      });

同样的reject的简写方式也和resolve一样

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      reject("失败的值");
    });
    
    //简写为
    Promise.reject('失败的值')

一般我们在实际项目中一般会这样写

      ...
      //网络请求中获取到数据后
      if(xxx){
        //成功
        return Promise.resolve('请求的值')
      }
      return Promise.reject('失败原因')
      ...

其实.then中也会自动返回Promise的封装,也就是说这个链式调用我们可以直接这样写

    new Promise((resolve, reject) => {
      //这里一般会有一个网络请求或其它异步操作
      resolve("成功的值1");
    })
      .then((res) => {
        console.log(res); //成功的值1
        return "成功的值2";
      })
      .then((res) => {
        console.log(res); //成功的值2
        return "成功的值3";
      })
      .then((res) => {
        console.log(res); //成功的值3
        //以此类推...
      });

以上便是Promise的链式调用,Promise的链式调用一般用于这些步骤间有先后顺序的操作,比如开头举的例子,需要使用前一个接口请求的数据作为参数去请求另一个接口的情形。

Promise中的all函数

在实际项目中你是否遇到过这样一个情况:你有A、B、C三个接口(或则更多),C接口的参数需要用到A和B两个接口的结果值,此时你为怎么做?

先请求A接口再请求B接口最后再根据AB接口的结果去请求C接口

    new Promise((resolve, reject) => {
      //请求A接口,这里用setTimeout模拟请求
      setTimeout(() => {
        resolve("A的结果");
      }, 100);
    })
      .then((res) => {
        //根据A结果请求B接口
        setTimeout(() => {
          return "B的请求结果";
        }, 100);
      })
      .then((res) => {
        //根据A和B结果请求C接口
        setTimeout(() => {
          console.log("C的请求结果");
        }, 100);
      })
      .catch((err) => {
        //这里暂不做错误考虑
      });

这种写法逻辑上是没问题的,但是B和A的请求之间是完全没有交集的,而浏览器的http请求是可以同时发起多个请求的,所以这种写法很明显增加了接口请求时间

在每个请求结束后都去调用请求C的函数,在这个函数中判断两个请求的数据是否都获取到了,然后再进行处理

    let isResultA = false;
    let isResultB = false;

    //请求A接口,这里用setTimeout模拟请求
    setTimeout(() => {
      isResultA = true;
      getC()
    }, 100);

    //请求B接口,这里用setTimeout模拟请求
    setTimeout(() => {
      isResultB = true;
      getC()
    }, 100);
    function getC() {
      if (isResultA && isResultB) {
        //根据A和B的结果请求C接口数据
        setTimeout(() => {
          console.log("C的请求结果");
        }, 100);
      }
    }

很显然这种在写法上是很麻烦的,所以Promise提供了all方法

Promise.all接收一个iterable类型(Array,Map,Set 都属于 ES6 的 iterable 类型),可以放多个Promise实例,最后.then中获得的是这些输入的Promise的resolve回调的结果数组。同时只要任何一个输入的Promise的reject回调执行或者输入不合法的Promise就会立即抛出错误

    Promise.all([
      new Promise((resolve, reject) => {
        //请求A接口,这里用setTimeout模拟请求
        setTimeout(() => {
          resolve("A的结果");
        }, 2000);
      }),
      new Promise((resolve, reject) => {
        //请求B接口,这里用setTimeout模拟请求
        setTimeout(() => {
          resolve("B的结果");
        }, 1000);
      }),
    ])
      .then((res) => {
        console.log(res[0]); //A的结果
        console.log(res[1]); //B的结果
        //根据A和B的结果请求C接口数据
        setTimeout(() => {
          console.log("C的请求结果");
        }, 100);
      })
      .catch((err) => {
        console.log(err);
      });

Promise中的race函数

Promise.race方法返回一个promise,一旦迭代器中的某个promise完成,返回的promise就会被完成。简单来说就是它接收的promise实例中谁快就用谁的结果,不管你的结果是resove的还是reject

    Promise.race([
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("结果1");
        }, 1000);
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("结果2");
        }, 500);
      }),
      new Promise((resolve, reject) => {
        //请求B接口,这里用setTimeout模拟请求
        setTimeout(() => {
          reject("结果3");
        }, 100);
      }),
    ])
      .then((res) => {
        //不会触发
        console.log(res);
      })
      .catch((err) => {
        console.log(err); //结果3
      });

上面示例很显然第三个Promise示例最先返回结果,所以Promise.race便使用了第三个Promise的结果

Promise中的any函数

Promise.any函数它也接收一个Promise实例的可迭代对象,只要其中的一个promise实例成功,就返回那个已经成功的promise,只有所有的promise实例都失败才会返回失败的(reject)的数组

    Promise.any([
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("结果1");
        }, 1000);
      }),
      new Promise((resolve, reject) => {
        setTimeout(() => {
          reject("结果2");
        }, 500);
      }),
      new Promise((resolve, reject) => {
        //请求B接口,这里用setTimeout模拟请求
        setTimeout(() => {
          reject("结果3");
        }, 100);
      }),
    ])
      .then((res) => {
        //不会触发
        console.log(res);
      })
      .catch((err) => {
        console.log(err); //AggregateError: All promises were rejected
      });

这个函数适用的场景可能不是很多,在这里我大概想到的一个场景就是:有三个接口A,B,C,这三个接口很不稳定但是它们返回的成功结果都一样,所以我们需要对这三个接口进行同时请求,只要它们其中有一个接口返回成功,那么我们便用这个接口的值。所以这三个接口只要有一个可用我们便可拿到想要的结果

async和await

async和await其实就是promise的语法糖形式,它可以让我们的异步代码包装成同步的形式理解。await顾名思义就是等待的意思,它必须使用在一个async的函数中,await后面跟的是一个实例化Promise,它返回的值则是这个Promise成功返回的 resolve 状态值。其实它的用法很简单,如下

      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve("结果");
        }, 1000);
      });
    };

    const getData = async () => {
      const res = await promiseFun();
      console.log(res);//结果
    };
    getData();

如果我们把文章开头的axios请求例子改为async,await的形式它将会是这个样子

    const getAxiosData = async () => {
      try {
        const res1 = await axios.get(url1, {});
        const res2 = await axios.get(url2, { query: res1.xxx });
        const res3 = await axios.get(url2, { query: res2.xxx });
        console.log(res3);
      } catch (err) {
        console.log(err);
      }
    };
    getAxiosData();

此时的代码逻辑看起来就会清晰很多

Promise的大致用法基本也就介绍完了,其实Promise还涉及到另一个方面的知识事件循环(Event Loop) 还有宏任务微任务等,由于篇幅原因,这部分我会抽时间单独写一篇关于这方面的文章。同时如果你发现文中有错误或不妥的地方欢迎指出,一定及时修改,感谢~

创作不易,你的点赞就是我的动力!如果感觉这篇文章对你有所帮助的话就请点个赞吧,感谢orz


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK