JS(15)一文搞定 Promise

104 阅读4分钟

JS 原生异步

JS 是支持异步的,但是原生 JS 支持的是通过给异步函数传参(回调函数)实现的异步操作。

它的缺陷还是蛮多的:

1. 回调嵌套

在项目中,我们往往需要函数嵌套函数执行,可能比上述代码更为难以理解,所以就需要我们花费很多精力去思考它们的执行顺序。what's worse?实际上,我们还会在代码中加入各种各样的逻辑判断,就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新。

doA( function(){
    doB();

    doC( function(){
        doD();
    } )

    doE();
} );

doF();

2. 无法获悉回调函数执行情况

当我们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API是如何封装的,可能会出现以下情况:

  1. 回调函数执行多次
  2. 回调函数没有执行
  3. 回调函数有时同步执行有时异步执行

而对于这些情况,我们都需要相应的处理措施!!

3. 回调地狱

  • 代码难以复用

回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。

  • 堆栈信息被断开

同步执行:如果在A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。所以说,如果出错了,执行上下文栈是还保存的

异步执行:,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。

【Promise没有解决该问题】

Promise

Promise 是 JavaScript 中用于处理异步操作的对象,它提供了一种更优雅和可读性更高的方式来处理异步代码。

Promise 对象有三个状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

实例化 Promise 对象

(1) new 构造函数创建
let p1 = new Promise(function (resolve, reject) {
    if (a > 4)
        return resolve(a);
    else
        return reject(`${a}small`)
})

构造函数的参数 ---- 执行器函数 。该函数参数有两个函数参数resolve() 、reject() ,调用两者,可以分别实现不同的状态改变!!!

注意:

Promise构造函数本身是同步的立即执行的函数,所以构造函数内部的代码是立即执行的,但在内部遇到 resolve()、reject() 函数时 ,才会进行异步操作,因为执行完resolve()、reject() 函数后,就会立马改变Promise的状态,就会调用.then() 方法中注册的处理函数,而这个回调函数属于微任务,会在本轮事件循环的末尾执行。

(2) Promise 原型方法创建状态已经确定的实例对象
  • Promise.resolve()

实例化一个解决的期约,且该期约的值就是传入的第一个参数

  • Promise.reject()

实例化一个拒绝的期约,且期约的拒绝理由就是传入的第一个

Promise 的一些回调处理程序【会在期约状态发生改变后立即执行】(状态改变的过程其实就是异步任务执行的过程)

  • Promise.prototype.then()

该方法接收两个参数 onresolved() 、onRejected() 两个处理程序,如果提供的话,会在期约进入解决or拒绝状态时执行,因为Promise状态只能改变一次,所以两个处理程序是互斥的!!!

  • Promise.prototype.catch()

.catch() 方法用于给期约添加期约拒绝处理程序 onRejected(),相当于一个语法糖,调用它就相当于调用 Promise.prototype.then(null , onRejected())

注意: 如果.then() 方法中已经给出了 onRejected() 处理程序,就默认不会执行 .catch() 方法了!!

  • Promise.prototype.finally()

该方法用于给期约添加 onFinally() 处理程序 ,该程序会在期约的状态发生改变后执行(不管是哪种改变都会执行),常常用于添加清理代码!!!

注意:

  • onFinally() 处理程序是不接受参数的!!!
  • 而 onresolved() 、onRejected() 两个处理程序会默认接受一个参数,且参数为解决或者拒绝期约的value值!!!
 let a = 3;
 let p1 = new Promise(function (resolve, reject) {
     if (a > 4)
         return resolve(a);
     else 
         return reject(`${a}small`)
 }).then((b) => {
     console.log(`${b}很大`)
 }, (b) => {
     console.log(`${b}很小`)
 }).catch((b) => {
     // console.log(a)
 }).finally((c) => {
     console.log(c)
 })
 
 // 3small很小
    undefined

期约连锁与合成

(1) 期约连锁

期约的 .then() .catch() .finally() 方法都会返回一个新的期约实例,而该实例也会有自己的 .then() .catch() .finally() 方法 ,so就会形成期约连锁!!!

2428.png

注意:

  • 前提是得返回一个期约才能继续连锁
(2) 期约合成

将多个期约实例合成一个期约实例的方法

  • Promise.all()

      - 接收一个可迭代对象作为参数,可迭代对象中的元素会依次通过 Promise.resolve()转换为期约
      - 有待定  ---->  待定
      - 全解决  ---->  解决
      - 有拒绝  ---->  第一个拒绝
    
  • Promise.race()

       - 接收一个可迭代对象作为参数
       - 合成为最先解决or拒绝的期约
    

Promise 的局限性

(1) Promise 内部的错误被吃掉
javascript
复制代码
let promise = new Promise(() => {
    throw new Error('error')
});
console.log(2333333);

会正常的打印 233333说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。

(2) 同步异步执行的二元性【拒绝的期约无法被 try catch块捕捉】
try{

Promise.reject( new Error("bar"))

}catch(e){

console.log(e)

} // Uncaught....

可见 try/catch 同步代码并没有捕捉到期约抛出的错误,原因如下:

拒绝期约抛出的错误并没有抛到执行同步代码的线程中,而是通过浏览器异步消息队列来处理的。代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构 --- Promise,同步代码是与之交互不了的!!!

(3) 无法取消

Promise 一旦新建它就会立即执行,无法中途取消。

(4) 无法得知 pending 状态

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。