亲宝软件园·资讯

展开

JavaScript promise

​ 陈梵阿   ​ 人气:0

为什么要用Promise?

我们知道JavaScript是单线程的,一次只能执行一个任务,会阻塞其他任务。因此,所有的网络任务、游览器事件等都是异步的,我们可以使用异步回调函数来进行异步操作。

有这么一个场景,我可以通过6个人能够认识到任何一个人。但是我们不知道当前的人联系到下一个人的时间是多久,假如这是一个异步的操作。

可以用如下代码表示:

  function ConnectPeople(i) {
    console.log(`我联系到了第${i}个人`);
    return i + 1;
  }
  let i = 1;
  setTimeout(() => {
    const result1 = ConnectPeople(i);
    setTimeout(() => {
      const result2 = ConnectPeople(result1);
      setTimeout(() => {
        const result3 = ConnectPeople(result2);
        setTimeout(() => {
          const result4 = ConnectPeople(result3);
          setTimeout(() => {
            const result5 = ConnectPeople(result4);
            setTimeout(() => {
              const result6 = ConnectPeople(result5);
              setTimeout(() => {
                const result7 = ConnectPeople(result6);
                setTimeout(() => {
                  const result8 = ConnectPeople(result7);
                  setTimeout(() => {
                    const result9 = ConnectPeople(result8);
                    setTimeout(() => {
                      const result10 = ConnectPeople(result9);
                    }, 10000);
                  }, 5000);
                }, 3000);
              }, 2000);
            }, 3000);
          }, 2000);
        }, 1000);
      }, 500);
    }, 2000);
  }, 1000);

image.png

如上所示,当我们联系到了第一个人后,再去联系第二个人,然后再去联系第三个人...直到我联系到了10个人。乍一看,代码好像还挺规整,但是如果100个人,1000个人呢?由于回调很多,函数作为参数层层嵌套,就陷入了回调地狱。这种情况下,就像是金字塔一样的代码非常不利于阅读。

但是还好,我们有解决办法。

使用Promise解决异步控制问题

什么是Promise?

Promise对象的主要⽤途是通过链式调⽤的结构,将原本回调嵌套的异步处理流程,转化成“对象.then().then()...”的链式结构,这样虽然仍离不开回调函数,但是将原本的回调嵌套结构,转化成了连续调⽤的结构,这样就可以在阅读上编程上下左右结构的异步执⾏流程了。

因此,Promise的作⽤是解决“回调地狱”,他的解决⽅式是将回调嵌套拆成链式调⽤,这样便可以按照上下顺序来进⾏异步代码的流程控制。 如下代码所示,我们使用了Promise,代码也从原先的金字塔形式转变成了从上往下的执行流程。

  function ConnectPeople(i) {
    console.log(`我联系到了第${i}个人`);
    return i + 1;
  }
  const p = new Promise((resolve) => {
    setTimeout(() => {
      resolve(ConnectPeople(1));
    }, 1000);
  });
  p.then((v1) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(ConnectPeople(v1));
      }, 1000);
    });
  }).then((v2) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(ConnectPeople(v2));
      }, 1000);
    });
  });

image.png

Promise的结构

根据上面的代码案例,我们发现Promise需要通过new关键字同时传入一个参数来创建,所以我们可以尝试打印一下window对象console.log(window)(window 对象在浏览器中有两重身份,一个是ECMAScript 中的 Global 对象,另一个就是浏览器窗口的 JavaScript 接口),可以发现存在一个Promise的构造函数。

image.png

Promise初始化的时候需要传入一个函数,如下所示:

const p = new Promise(fn) // fn是初始化的时候调用的函数,它是同步的回调函数

回调函数

什么是回调函数?

JavaScript中的回调函数结构,默认是同步的结构。由于JavaScript单线程异步模型的规则,如果想要编写异步的代码,必须使⽤回调嵌套的形式才能实现,所以回调函数结构不⼀定是异步代码,但是异步代码⼀定是回调函数结构。

为什么异步代码一定是回调函数结构?

我们知道JavaScript是单线程异步模型,严格按照同步在前异步在后的顺序执行。如果用默认的上下结构,我们拿不到异步回调中的结果。

如下所示,代码执行的时候会先执行同步代码,异步代码会挂起,继续执行同步代码,到1s的时候挂起的任务会进入队列,到2s的时候会继续执行同步代码打印1,然后从任务队列中取任务将num变成100,打印num。 所以实际执行效果是,过2秒后,先打印1再打印100。

var num = 1;
setTimeout(()=>{
    num = 100
    console.log(num)
},0)
var d = new Date().getTime()
var d1 = new Date().getTime()
while ( d1 - d < 1000 ) {
    d1 = new Date().getTime()
}
console.log(num) // 1

刨析Promise

翻译一下promise,结果是承诺,保证,红宝书中的解释是期约

它有三个状态:

三种状态之间的关系:

当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏。

image.png

我们打印一下Promise对象,发现它的构造函数中定义了allallSettledanyracerejectresolve方法(这些是实例方法),它的原型上存在catchfinallythen方法(这些是原型方法)。

原型方法——catch\finally\then

首先看下面代码:

  new Promise(function (resolve, reject) {
    resolve();
    reject();
  })
    .then(function () {
      console.log("then执⾏");
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

执行后依次打印then执行->finally执行,发现.catch的回调没有执行。

再看如下代码:

  new Promise(function (resolve, reject) {
    reject();
    resolve();
  })
    .then(function () {
      console.log("then执⾏");
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

这个串代码和之前的代码唯一的不同在于Promise中的回调先执行了resolve()还是先执行了reject(),打印结果是catch执行->finally执行,发现.then的回调没有执行。

那如果Promise的回调不执行reject()和resolve()呢?

会发现什么输出都没有!

注意:Promise.prototype.catch()其实是一个语法糖,相当于是调用 Promise.prototype.then(null, onRejected)。.then中其实是可以传入2个回调函数,第一个回调函数是resolve()后执行,第二个回调函数是reject()后执行,2个是互斥的。

这是因为Promise的异步回调部分如何执⾏,取决于我们在初始化函数中的操作,并且初始化函数中⼀旦调⽤了resolve后⾯再执⾏reject也不会影响then执⾏,catch也不会执⾏,反之同理。

⽽在初始化回调函数中,如果不执⾏任何操作,那么promise的状态就仍然是pending,所有注册的回调函数都不会执⾏。

由此可见,执行完resolve()之后才能够执行.then的回调;执行reject()之后才能够执行.catch的回调;finally()的回调会在执行完.then或.catch之后执行。

这时候,我们就会想,是不是可以把resolve或者reject的调用设定在异步函数内去调用,这样是不是就能解决回调地狱的问题了?

所以我们就去尝试一下:

  new Promise(function (resolve, reject) {
    setTimeout(() => {
      console.log(111);
      resolve();
    }, 2000);
  })
    .then(function () {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          console.log(222);
          resolve();
        }, 2000);
      });
    })
    .then(function () {
      return new Promise(function (resolve, reject) {
        setTimeout(() => {
          console.log(333);
          resolve();
        }, 2000);
      });
    })
    .catch(function () {
      console.log("catch执⾏");
    })
    .finally(function () {
      console.log("finally执⾏");
    });

上面代码每隔2s依次打印111->222->333 finally执行。333执行后立马执行finally。

为什么要在.then的回调函数中return一个Promise呢?

因为下一个异步的执行,需要等待前一个异步执行完毕后才调用,我们需要用到resolve来控制.then执行的时机。

那如果我们不指明return返回值,它会返回什么呢?是如何实现链式调用呢?

看下面代码:

  const p2 = new Promise((resolve, reject) => {
    resolve();
  });
  const p3 = p2.then(() => {
    console.log("resolved");
  });
  console.log(p3, 111);

p2.then的回调函数中没有return,但是我们知道一般来说函数返回值默认返回undefined,但是undefined中不会存在.then的方法。

因此我们就看一下p3里面到底是什么。有些人会想,.then是异步调用的,它是一个微任务,那访问p3是不是不太正确?

我们打印一下p3,就会看到如下信息:

image.png

第一行p3的状态还是pending,当我们点开,发现已经变成了fulfilled了,因为引用类型是按地址访问的,当我们点开的时候会发现指向这个地址里最后的数据是什么。普通对象同理。

如下所示,我们console的时候a对象的name还是a,但是我们点开后发现程序执行完后a对象的实际name变成了b。

  const a = { name: "a" };
  console.log(a);
  a.name = "b";

image.png

回归正传,我们发现p3里面有3个字段,[[Prototype]]我们很熟悉,这个是一个指向当前对象原型的指针。在大多数游览器中是可以通过__proto__访问到的。

我们尝试着去访问:

  console.log(p3, 111);
  console.log(p3.__proto__);
  console.log(p3.__proto__ === Promise.prototype); // true

image.png

我们可以看到.then默认返回的有3个字段,然后通过原型链来实现链式调用:

本质就是在我们调⽤这些⽀持链式调⽤的函数的结尾时,他⼜返回了⼀个包含他⾃⼰的对象或者是⼀个新的⾃⼰,这些⽅式都可以实现链式调⽤。

中断链式调用的方式:

中断的⽅式可以使⽤抛出⼀个异常或返回⼀个rejected状态的Promise对象

链式调用的基本形式:

前面几条我们都能懂,第5条什么意思的? 看下面代码:

  const p2 = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
  });
  p2.then(() => {
    console.log(2);
    return 123;
  })
    .then()
    .then("456")
    .then((res) => {
      console.log(res);
    });

image.png

发现只打印了1 2 和 123,return的123进入了最后一个.then的回调函数中作为参数。

resolve和reject

至于resolve和reject,我们通过上面已经知道了resolve和reject能够更改Promise的状态,而Promise的状态是不可逆的,且是私有的。所以我们必须在Promise内部调用resolve或者reject。

当然,resolve和reject也能够传入参数,而传入的参数,会变为.then或.catch的回调函数中的参数。

那如果传入一个Promise作为参数呢???

resolve()

实际上,如果在resolve中传入一个promise,那它的行为就相当于是一个空包装。Promise.resolve()可以说相当于是一个幂等方法,会保留传入期约的状态。

let p = Promise.resolve(7);
setTimeout(console.log, 0, p === Promise.resolve(p)); // true
setTimeout(console.log, 0, p === Promise.resolve(Promise.resolve(p))); // true

reject()

会实例化一个拒绝的期约并抛出一个异步错误,不能通过try...catch捕获,只能通过拒绝处理程序捕获。

如果给reject传入一个promise,则这个promise会成为返回的拒绝promise的理由。

  const p1 = new Promise(() => {});
  const p2 = Promise.resolve(111);
  const r3 = Promise.reject(p1);
  const r4 = Promise.reject(p2);
  console.log(r3);
  console.log(r4);

image.png

Promise常用API——all()、allSettled()、any()、race()

all()

假如我们有一个需求,一个页面需要请求3个接口才能渲染,并且要求3个接口必须全部返回。如果我们通过链式调用的方式,接口1请求了再去请求接口2然后去请求接口3,全都成功了再去渲染页面。这种就很耗时,所以就有了一个all的方法来解决。

Promise.all([promise对象,promise对象,...]).then(回调函数)

Promise.all()的参数是一个Promise数组,只有数组中所有的Promise的状态变成了fulfilled之后才会执行.then回调的第一个回调函数,并且将每个Promise结果的数组变为回调函数的参数。如果Promise中有一个rejected,那么就会触发.catch()的回调。

  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼀个promise执⾏完毕");
    }, 1000);
  });
  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼆个promise执⾏完毕");
    }, 2000);
  });
  let p3 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第三个promise执⾏完毕");
    }, 3000);
  });
  Promise.all([p1, p3, p2])
    .then((res) => {
      console.log(res);
    })
    .catch(function (err) {
      console.log(err);
    });
    // 3s后打印 ['第⼀个promise执⾏完毕', '第三个promise执⾏完毕', '第⼆个promise执⾏完毕']

race()

race()方法与all()方法的使用格式相同,不同的是,回调函数的参数是promise数组中最快执行完毕的promise的返回值,它的状态可能是fulfilled也有可能是rejected,但是是最快返回的。

根据race这个单词就能理解,相当于一群promise进行比赛,谁先到终点第一就是谁,不管是男是女。

  //promise.race()相当于将传⼊的所有任务进行一个竞争
  let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("第⼀个promise执⾏完毕");
    }, 5000);
  });

  let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("第⼆个promise执⾏完毕");
    }, 2000);
  });
  let p3 = new Promise((resolve) => {
    setTimeout(() => {
      resolve("第三个promise执⾏完毕");
    }, 3000);
  });
  Promise.race([p1, p3, p2])
    .then((res) => {
      console.log(res);
    })
    .catch(function (err) {
      console.error(err);
    });
    // 2秒后打印第二个promise执行完毕

allSettled()

该方法需要传入所有不在pendding状态的promise数组,然后通过该方法可以知道数组中的promise的当前状态。

当有多个彼此不依赖的异步任务成功完成时,或者总是想知道每个promise的结果时,通常使用它。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];

Promise.allSettled(promises).
  then((results) => results.forEach((result) => console.log(result.status)));

// "fulfilled"
// "rejected"

any()

这个方法目前还是实验性的,不是所有的游览器都能够支持。

接受一个promise数组,只要有一个promise的状态变成了fulfilled,那么这个方法就会返回这个promise;

如果所有的promise的状态都是rejected,那么就返回失败的promise,并且把单一的错误集合在一起。

const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
});
const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
});
const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});
Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);
})
// 很快完成

加载全部内容

相关教程
猜你喜欢
用户评论