上节课我们介绍了Promise中的常用API,这节课我们来展开说一说Promise中的几个关键问题,这几个关键问题是为了后面封装方面的内容来做基础支撑的。

修改Promise状态

首先第一个问题,我们怎么修改Promise对象的状态?我在之前的课程中也已经说过了,我们不能够手动去修改Promise对象的状态,但是是不是没法修改呢?当然不是了。或许大家有人学过Java,知道Java中的属性有一种权限叫private,这个权限不允许我们手动去直接修改属性,但是我们会在类中预留一个方法,然后我们通过这个方法来去修改或者读取某个private的属性。

Promise中也做了类似的设计。就是我们之前一直再用的resolve函数和reject函数。

  • resolve()使得Promise对象状态从pending => fulfilled
  • reject()使得Promise对象状态从pending => rejected

但是出了这两种方法还有没有其他方法来改变Promise对象的状态呢?其实是有的。我们来看一下:

let p = new Promise(
  (resolve, reject) => {
    throw "Error";
  }
);
console.log(p);

我们来看一下这段代码,我们在Promise中直接抛出一个异常,那么结果会是什么样子呢?我们来看一下:

image-20220314143831856

我们看到控制台中输出的Promise对象的状态是rejected,而结果就是我们抛出的异常字符串。羡慕这个报错我们也说过,这是因为我们没有指定失败回调导致的。

所以说Promise对象的状态可以通过以上三种方法来修改。

Promise能否执行多个回调

我们在初始化了Promise对象之后,都会来指定相应的回调函数,那么回调函数我们都是在哪指定的呢?我们上节课中在介绍到then方法和catch方法的时候就说到了,这两个方法是用于指定回调函数的,所以说我们的所有回调函数都是在then或者catch方法中来进行指定的。那么如果我给一个Promise对象指定了多个回调函数,那么这些回调函数会都被执行吗?我们来试一下:

let p = new Promise(
  (resolve, reject) => {
    resolve();
  }
);
p.then(()=>console.log("Callback 01"));
p.then(()=>console.log("Callback 02"));

我们现在这段代码是让这个Promise对象是一个成功的Promise,然后我们接下来指定了两个成功的回调,那么我们来看一下结果是怎样:

image-20220314144853776

我们来看一下,控制台中成功输出了两个回调的执行结果,所以说当我们给一个Promise对象制定了多个回调的话,那么对应状态的回调都会被执行。

Promise状态修改和指定回调的先后顺序

我接下来再来思考一个问题,我们在Promise对象的状态修改和回调指定到底谁先执行呢?我们通常通过new Promise(excutor)来实例化一个Promise对象,我们基本都是在excutor函数中执行异步任务,并调用resolve或者reject函数来修改状态,而我们是在实例化Promise对象之后再调用then或者catch来指定回调函数。

但是Promise对象的状态修改和指定回调的先后顺序真的就像我们上面所说的那样吗?有没有可能回调函数的指定在状态修改之前呢?其实是有可能的。而且先指定回调函数再修改状态,和先改变状态再指定回调函数这两种情况都是存在的。

既然两种情况都会存在那么什么时候会先改变状态,什么时候又会先指定回调呢?薛定谔的嘛?当然不是,当我们在excutor中直接调用resolve或者reject函数时这将是一个同步任务,这个时候会先改变状态。状态改变之后马上指定回调,然后根据状态调用回调函数获取到任务执行完成后的数据。

如果我们在excutor中开启一个异步任务的话,那么我们将会先指定回调函数,等异步任务执行完成后,状态修改,然后调用回调函数,获取到任务执行完后的数据。

then方法返回的新Promise对象

我们之前提到过,then方法在被调用之后是会生成一个新的Promise对象的。但是这个新的Promise对象的状态还有结果都是由什么决定的呢?接下来我们就来展开说一说。

首先我来看一下代码:

let p = new Promise(
  (resolve, reject) => {
    resolve();
  }
);
let result = p.then(
  () => console.log("Callback 01")
);
console.log(result);

我们这段代码干了什么?首先我们实例化了一个成功的Promise对象,然后在then方法中指定了成功时的回调函数,但是我的关注点不在这,我们关注点是我们声明一个变量来接收then方法的返回值,并打印出来。

image-20220314152458276

我们来看,控制台输出了一个对象。这正如我们之前说的会返回一个新的Promise对象,但是这个对象的状态和结果都是什么呢?其实then方法返回的Promise对象状态和结果都由执行的回调函数来决定。

什么意思呢,就是说不管then方法中指定的成功的回调以及失败的回调哪一个被执行,那么被执行的回调函数的执行结果将决定then方法返回的Promise对象的状态和结果。

那么回调函数执行的结果有几种情况呢?

  • 抛出异常
  • 返回非Promise类型的结果
  • 返回Promise对象

可能有人要说还有没有返回值的情况,但是我们都知道js函数没有返回值代表着返回undefined,这也属于非Promise类型的结果。所以说一个函数被执行之后结果只有以上三种情况。那么我们来分贝测试一下:

let p = new Promise(
  (resolve, reject) => {
    resolve();
  }
);
let result = p.then(
  () => { throw "error"; }
);
console.log(result);

首先我们让回调函数抛出异常看一下结果:

image-20220314153709494

生成的Promise对象是一个失败的Promise,而且结果值就是我们的异常字符串。

let p = new Promise(
  (resolve, reject) => {
    resolve();
  }
);
let result = p.then(
  () => { return 123; }
);
console.log(result);

这一次我们让回调函数返回一个非Promise类型的数据看一下结果:

image-20220314154046706

控制台中输出了一个成功的Promise对象,而Promise对象的结果就是我们的返回值。

let p = new Promise(
  (resolve, reject) => {
    resolve();
  }
);
let result = p.then(
  () => { 
    return new Promise((resolve,reject)=>{
      resolve("OK");
    }); 
  }
);
console.log(result);

这一次呢我们让回调函数返回一个Promise对象,这次返回的是一个成功的Promise,那我们来看一下结果:

image-20220314164408909

生成的Promise对象也是一个成功的Promise,而且结果也是我们回调函数返回的Promise对象的结果值,那么如果我们的回调函数返回的是一个失败的Promise对象呢?

let p = new Promise(
  (resolve, reject) => {
    resolve();
  }
);
let result = p.then(
  () => { 
    return new Promise((resolve,reject)=>{
      reject("error");
    }); 
  }
);
console.log(result);

我这次再看一下结果:

image-20220314164626012

这次then方法生成的Promise对象就是一个失败的Promise,而且结果也是回调函数返回的Promise对象的结果。

Promise串联任务

这个词或许大家比较陌生,什么叫串联任务呢?我们之前不是说then方法会返回一个新的Promise对象嘛,那么这个新的Promise对象是不是也可以调用一个then方法?那么我们是不是直接链式调用像这样p.then(function).then(function)...就可以了?这一步操作就是我们所谓的串联任务。

那么我们来演示一下:

let p = new Promise(
  (resolve, reject) => {
    resolve(123);
  }
);
let result = p.then(
  () => {
    return new Promise((resolve, reject) => {
      resolve("OK");
    });
  }).then(
  value => console.log(value)
);

我们来看上面这一段代码,我们首先实例化一个Promise对象,这个对象里面可以执行同步任务也可以执行异步任务,然后再指定回调函数的时候,我们可以在回调中再开一个Promise来执行一个新的同步或者异步任务。当then方法调用完成之后,是不是获得了一个新的Promise对象,那么这个对象是不是可以再调用then方法?那么这个时候我们再在then方法中指定回调函数,这样的话就实现了then方法的链式调用,也就实现了串联任务,因为我们通过一次链式调用把两个任务给串联起来了。

image-20220314170153227

我们来看一下,控制台中也成功输出了链式调用最后一节的回调的执行结果。

Promise异常穿透

我们接下来来介绍另一个新的名词,叫异常穿透,那么什么叫做异常穿透呢?比如我们做了then方法的链式调用,但是谁都没法保证在整个调用链的任何一个环节都不会报错。既然可能报错,那我们肯定得捕获异常,但是每个环节都写一个异常捕获机制的话也太麻烦了,这样的话Promise的优势也没法体现出来,所以说我们可以用异常穿透来在整个调用链的最后一环来捕获异常:

let p = new Promise(
  (resolve, reject) => {
    setTimeout(() => { resolve("OK") }, 2000);
  }
);
let result = p.then(
  value => {
    return new Promise((resolve, reject) => {
      console.log(value)
      setTimeout(() => { reject("error") }, 100)
    });
  }).then(
  value => console.log(value)
).then(
  value => console.log(value)
).catch(
  reason => console.warn(reason)
);

我们来看一下,我们在then链式调用的最后一环调用了catch方法,那么们来让整个调用链的其中一环失败来看一下结果:

image-20220314171322877

我们看见我们是在第二环的异步任务失败了,然后打印出了失败的原因。那么我们如果是回调抛出异常了呢?

let p = new Promise(
  (resolve, reject) => {
    setTimeout(() => { resolve("OK") }, 2000);
  }
);
let result = p.then(
  value => {
    return new Promise((resolve, reject) => {
      setTimeout(() => { resolve("OK!") }, 100)
    });
  }).then(
  value => { throw "error"; }
).then(
  value => console.log(value)
).catch(
  reason => console.warn(reason)
);

我们现在让调用链的第三环抛出异常再来看一下结果

image-20220314171733291

这次我们看到控制台也成功不活了异常并执行了指定的回调。

Promise链的中断

首先什么是Promise链?我们如果之前用了then方法的链式调用,就会形成一个调用的链状结构,这个链状结构就是我们所谓的Promise链。我们来看一下我们现在的代码:

let p = new Promise(
  (resolve, reject) => {
    setTimeout(() => { resolve(111) }, 2000);
  }
);
let result = p.then(
  value => {
    console.log(value)
    return new Promise((resolve, reject) => {
      setTimeout(() => { resolve(222) }, 100)
    });
  }).then(
  value => console.log(value)
).then(
  value => console.log(value)
).catch(
  reason => console.warn(reason)
);

现在我们这个Promise链现在输出的情况是什么呢?

image-20220314172313728

是不是除了catch方法那一环以外,其他的都输出了,因为我们没有抛出异常也没有失败的Promise,所以catch方法并没有被调用。那么我们来看一下如果说我希望我们的Promise链仅仅只执行完第一环就中断的话,应该怎么处理呢?我们说过then方法被调用后会生成一个新的Promise对象,而这个Promise对象有几种状态?是不是三种?而then方法是不是只能指定resolvedrejected的回调函数?那么我们如果返回一个在这两个状态之外的一个Promise对象是不是在接下来的then方法中什么回调都匹配不上啊?那么这样的话,是不是就可以直接中断我们的Promise链了呢?我们来验证一下:

let p = new Promise(
  (resolve, reject) => {
    setTimeout(() => { resolve(111) }, 2000);
  }
);
let p1 = new Promise(() => {});
console.log(p1);
let result = p.then(
  value => {
    console.log(value)
    return new Promise(() => {});
  }).then(
  value => console.log(value)
).then(
  value => console.log(value)
).catch(
  reason => console.warn(reason)
);

我们这一次在第一环中返回了一个从来没见过的一个Promise对象,我们是不知道这个Promise对象到底是什么状态,所以我们单独声明这样一个Promise对象来输出看一下:

image-20220315084244116

首先我们看我们声明的这个Promise对象,它的状态是pending,那么在then方法中将会一个回调都匹配不到,所以就仅仅只执行了Promise链的第一环然后就中断了。

所以说中断一个Promise链的方法就是让then方法返回一个状态为pendingPromise对象。而且有且仅有这一种方法。

总结

以上便是Promise中的几个关键问题。

  • 修改Promise对象的状态有 3 种方法
  • resolve函数
  • reject函数
  • throw关键字
  • Promise对象某状态如果指定了多个回调,那么当Promise对象变更为该状态时,这些回调都讲被执行
  • Promise对象中如果封装的是同步任务,那么先改变状态,如果封装的是异步任务,则先指定回调
  • then方法返回的新Promise对象分为三种情况
  • 如果回调中抛出异常,then方法将返回失败的Promise对象,结果值为抛出的异常字符串
  • 如果回调函数返回一个非Promise对象的数据,then方法返回一个成功的Promise对象,结果值为回调的返回值
  • 如果回调函数返回一个Promise对象,then方法返回一个状态和结果值都与回调函数返回的Promise对象一致的新Promise对象
  • then方法的链式调用就是任务的串联,因为then方法可以返回一个新的Promise对象,而这个新的Promise对象中可以开新的任务
  • then方法的调用链的最后一环添加catch方法的调用可以捕获整条调用链中任何一环中的异常或者失败,这叫做异常穿透
  • 可以在then方法的调用链中的任何一环中返回一个状态为pandingPromise对象来中断Promise

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/import_question_promise/