『JS』 Promise 静态函数及错误处理

1,274 阅读12分钟

前言 🎙

大家好,我是潘小安!一个永远在减肥路上的前端er🐷 !

这篇文章是上一篇 手写 Promise 的补充部分🩹,主要是结合 demo 和大家聊聊Promise 常用的一些静态方法和异常处理,从而让我们在日常开发中更好的去理解和使用 Promise。话不多说(别的事留在小声 BB 章节聊),让我们从静态函数开始整!

静态函数 🤐

按照我的理解,静态函数总体可以分为两部分,一部分为 Promise 封装方法,包括 Promise.resolvePromise.reject。另一部分为 Promise 批处理方法,包括 Promise.allPromise.allSettledPromise.anyPromise.race,我们先从封装方法 Promise.resolve 开始聊。

Promise 封装方法

Promise.resolve

Promise.resolve 返回一个解析过的 Promise 对象。 Promise.resolve 方法参数分为三种情况,分别对应三种不同的返回值:

参数返回值
Promise 实例返回参数本身
thenable 对象展平 thenable 对象并返回
其他返回一个以参数为完成结果的 Promise 实例

我们可以通过几个 demo 来辅助理解 Promise.resolve 方法对不同参数如何处理:

参数为 promise:

let param = new Promise((resolve, reject) => {
  resolve('param is resoved promise')
})
let p = Promise.resolve(param)
console.log(p === param)//true

参数为 thenable 对象

对于 thenable 对象的一些吐槽可以翻阅《你不知道的 JS 中卷》第 188 页,文末会直接给到链接

因为我们日常使用 promise的时候,很少使用 thenable 对象,所以我们在写 demo 之前,先来看看 thenable 对象的定义:

thenable:任何含有then()方法的对象或函数.

参考手写 Promise 中的 resolvePromise 代码,我们可以知道,如果想让 promise 链得到结果,thenbale 对象需要长下面这样:

let thenable = {
  then: (resolve, reject) => {
      resolve('1')
  }
}

如果对 then 方法中的 resolvereject 感到困惑的小伙伴,可以看看点这里跳转到《手写 Promise》resolvePromise 方法内,当 xobject 或者 function,且 x.then 存在的代码,相信可以解答你的疑惑。

我们知道 thenable 作为参数的时候,Promise.resolve 会展平 thenable 对象并采用它的最终状态,考虑下面这种情况:

let thenable = {
    then: (resolve, reject) => {
        onFulfill({
            then: (resolve, reject) => {
                resolve('1')
            }
        })
    }
}
let p = Promise.resolve(thenable)
console.log(p)//promise:{status:fulfilled,result:1}

所以我们需要避免在 thenablethen 方法中,使用 thenable 本身作为参数来调用 resolve 方法,因为这样会导致无限递归,考虑下面这种情况:

let thenable = {
  then: (resolve, reject) => {
    resolve(thenable)
  }
}

Promise.resolve(thenable)  // 死循环

这里需要注意的是,采用 thenable 的最终状态,包含了 thenable 抛出异常的情况,我们可以使用下面的 demo 来测试这种情况:

let thenable = {
    then: (resolve, reject) => {
       throw new Error('I am an erro')
    }
}
let p = Promise.resolve(thenable)
console.log(p)//Promise:promiseStatus:rejected,promiseResult:Erro,I am an erro

参数为其他值

当参数为其他值时,Promise.resolve 方法会返回以参数为 promiseResultpromiseStatusfulfilledPromise 实例。我们可以使用不同的基本类型写测试 demo 并查看结果:

let p=Promise.resolve(1)
let p1=Promise.resolve('1')
let p2=Promise.resolve(null)
let p3=Promise.resolve(undefined)
let p4=Promise.resolve(true)
console.log(p)
console.log(p1)
console.log(p2)
console.log(p3)
console.log(p4)

打印结果如下:

image.png

手写 Promise.resolve()

有了上面的 demo 结果辅助理解,我们可以尝试手写一下 Promise.resolve 的源码:

Promise.resolve = function (value) {
    // 处理参数为 Promise 的情况
    if (value instanceof Promise) return value;
    // 处理其他情况
    return new Promise((resolve) => {
      resolve(value);
    });
}

当参数为 Promise 实例,返回实例本身,否则返回一个新的 Promise 实例,参数作为值被 resolve 调用,结合之前 手写 Promise 的 resolve 方法补充resolve 方法会自动去展开 thenbale

Promise.reject

Promise.reject() 返回一个带有拒绝原因的 Promise 对象。

手写 Promise.reject()

Promise.reject() 方法比较好理解,就是返回一个结果拒绝原因 reason,状态为 rejectedPromise 实例。我们可以尝试写出它的实现源码:

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason)
    })
}

Promise 批处理方法

Promise 实例的批处理方法 的共同点就是参数为一堆 Promise 的实例,返回值是一个新的 Promise,返回值取决于参数中各个 Promise 实例的状态和结果;不同点在于规则,四个方法对参数中的多个 promise 的状态和结果有不同的规则,总结一下可以通俗的理解成以下表格(为了方便理解,将 promise 的状态由 pending=>fulfilledpending=>rejected):

方法通俗理解返回值
Promise.all一个就
Promise.allSettled不管对错,全执行完后返回每个的状态和结果
Promise.race不管对错,谁先执行完就返回谁
Promise.any有一个就返回,全错才返回

接下来我们就从 Promise.all 方法开始,深入的去了解每个方法的使用细节和源码实现。

Promise.all

Promise.all 有如下规则:

  • 参数需要为可迭代对象 iterable,如 ArraySring,否则抛出类型错误。
  • 可迭代对象为空,则同步返回一个状态为 fulfilledPromise 实例。
  • 可迭代对象不为空,但是不包含 Promise 实例,则异步返回一个 fulfilledPromise 实例。
  • 可迭代对象不为空,且包含 Promise 实例,则返回一个 pending 状态的 Promise 实例,该实例的状态变更条件为:
    • 所有参数中的 Promis 实例从 pending 状态变成 fulfilled 状态,返回值从 pending 状态变成 fulfilled 状态
    • 参数中有一个 Promise 实例从 pending 状态变成 rejected 状态,返回值从 pending 状态变成 rejected 状态
  • 返回值将会按照参数内的 promise 顺序排列,而不是由调用 promise 的完成顺序决定。

接下来我们使用 demo 来验证一下这些规则:

//参数需要为可迭代对象
let p = Promise.all(1)
console.log(p)//number 1 is not iterable 

//参数为迭代对象,但是为空,同步返回一个 Promise 实例,状态为 fulfilled
let p = Promise.all([])
console.log(p)//promise:{promiseState:fulfilled,promiseResult:[]}

//参数为迭代对象,但是不为空,不包含 Promise 实例,异步返回一个 Promise 实例,状态为 fulfilled

let p = Promise.all([1, 2, 3])
console.log(p)//promise:{ promiseState:pending }
setTimeout(function () {
    console.log(p)//promise:{promiseState:fulfilled,promiseResult:[1,2,3]}
})

//可迭代对象不为空,且包含 Promise 实例,则返回一个 pending 状态的 Promise 实例,状态根据参数中的 promise 的变更情况变更。
//参数中状态都变成 fulfilled 的情况
var p1 = Promise.resolve(1);
var p2 = Promise.resolve(2);
var p3 = Promise.resolve(3);
let pall = Promise.all([p1, p2, p3])
console.log(pall)//promise:{ promiseState:pending }
setTimeout(function () {
    console.log(pall)//promise:{promiseState:fulfilled,promiseResult:[1,2,3]}
}
)
//参数中存在 rejected 状态的 promise
var p1 = Promise.resolve(1);
var p2 = Promise.reject(2);
var p3 = Promise.resolve(3);
let pall = Promise.all([p1, p2, p3])
console.log(pall)//promise:{ promiseState:pending }
setTimeout(function () {
    console.log(pall)//promise:{promiseState:rejected,promiseResult:2}
}
)

最后一个测试用例我们单独列出来查看:

let p1 = new Promise((resolve, reject) => {
    resolve('1')
})
let p2 = new Promise((resolve, reject) => {
    resolve(new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve('3')
        })
    }))
})
let result = Promise.all([p1, p2, 3, {
    then: (resolve, reject) => {
        resolve('4')
    }
},])
console.log(result)//promise:{ promiseStatus:pending }
setTimeout(function () {
    console.log(result)//promise:{promiseStatus:fulfilled,promiseResult:["1","2","3",4,"5"]}
})

在这个 demo 中,参数有以下特点:

  • 数组第一个变量 p1:一个状态为 fulfilledPromise 实例,promiseResult1
  • 数组第二个变量 p2:嵌套 promise,且嵌套定时器,最终的结果为状态为 fulfilledPromise 实例,promiseResult 的值为 2
  • 数组第三个变量:数字 3
  • 数组第四个变量:一个 thenable 对象 根据打印出来的结果,我们可以得到一个新的规则,即:

如果参数中的 promise 实例的 promiseResult 还是一个 Promise实例 或者 thenable 对象,会被展开

通过以上这些 demo,我们可以尝试来手写一下 Promise.all 的代码,代码中每段逻辑都使用注释标记.

Promise.all = function (params) {
    if (!(typeof params[Symbol.iterator] === 'function')) {
        throw new TypeError('params is not an iterator')
    }
    return new Promise((resolve, reject) => {
        //参数为空的时候,直接返回状态为 fulfilled 的 promise,result 为[]
        const final=[]
        let len = args.length;
        if (len === 0) return resolve(final);
        for (let i = 0; i < params.length; i++) {
            const item = params[i];
            //非 promise 的转换成 promise,then 方法可以展开所有的 promise 的嵌套
            Promise.resolve(item).then((result) => {
                final[i] = result;
                if (--len === 0) {
                    resolve(final);
                }
            }, (reason) => {
                // 一旦有promise被拒绝就立即拒绝
                reject(reason);
            });
        }
    });
};

Promise.allSettled

Promise.allSettled 方法返回一个对象数组,每个对象代表参数中 promise 的状态和值,其中使用 status 表示状态,若参数中的 promise 的最终状态是 fulfilled,则使用 value 表示该 promisepromiseresult,否则使用 reason 表示。

我们可以使用一个 demo 来尝试使用一下 Promise.allSettled 方法:

let p1 = new Promise((resolve, reject) => {
    resolve('1')
})
let p2 = new Promise((resolve, reject) => {
    reject('2')
})
let p3 = new Promise((resolve, reject) => {
    resolve(new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve('3')
        })
    }))
})
let p4 = new Promise((resolve, reject) => {
    reject(new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve('3')
        })
    }))
})
let p5 = {
    then: (resolve, reject) => {
        resolve({
            then: (resolve, reject) => {
                resolve('5')
            }
        })
    }
}
let p6 = {
    then: (resolve, reject) => {
        reject({
            then: (resolve, reject) => {
                resolve('5')
            }
        })
    }
}
let result = Promise.allSettled([p1, p2, p3, p4, p5, p6])
console.log(result)//promise:{promiseStatus:pending}
setTimeout(function () {
    console.log(result)
})
  /*promise:{promiseStatus:'fulfilled',promiseResult:[
 [
    {
        "status": "fulfilled",
        "value": "1"
    },
    {
        "status": "rejected",
        "reason": "2"
    },
    {
        "status": "fulfilled",
        "value": "3"
    },
    {
        "status": "rejected",
        "reason": promise:{promiseStatus:'fulfilled',promiseResult:3}
    },
    {
        "status": "fulfilled",
        "value": "5"
    },
    {
        "status": "rejected",
        "reason": {then:function(resolve,reject){
            resolve('5')
        }}
    }
]}*/

再看另外一个 demo:

let p=Promise.allSettled([])

console.log(p)//promise:{promiseStatus:'fulfilled',promiseResult:[]}

结合这两个 demo 可以看出,Promise.allSettledPromise.all 两个方法有许多共同点:

  • 当参数迭代器为空,同步返回状态为 fulfilled,值为 []Promise 实例。
  • 当参数迭代器中包含嵌套 promise 或者嵌套 thenable 对象的时候,若状态是 fulfilled,处理的时候会展开,换句话说,会递归拿到最终值;若状态是 rejected,则会直接返回。 不同点:
  • Promise.all 方法遇到 rejected 状态的时候,则直接修改结果的 Promise 实例状态为 rejected,原因和在参数中首次遇到的 rejected 状态的原因保持一致;
  • Promise.allSettled 方法遇到 rejected 状态的 Promise 实例时,只是单纯的记录下来,等所有的迭代器中的 Promise 状态更改完毕后,返回记录;

接下来我们可以尝试写写 Promise.allSettled 的源码:

Promise.allSettled = function (params) {
    if (!(typeof params[Symbol.iterator] === 'function')) {
        throw new TypeError('params is not an iterator')
    }
    const result = [];
    let count = 0;
    for (let i = 0; i < params.length; i++) {
        const item = params[i];
        //Promise.resolve 处理迭代器中的非promise 对象,把它们变成 promise
        //then 方法拿到 promise 的 result,并且可以展开循环嵌套
        Promise.resolve(item).then((result) => {
            result[i] = { status: 'fulfilled', value: result };
            if (++count === params.length) {
                resolve(result);
            }
        }, (reason) => {
            ++count;
            result[i] = { status: 'rejected', reason };
        });
    }
}

Promise.race

race 翻译过来就是比赛的意思,先到先返回。在接收的一组 promise 中,根据最快的一组决定返回的 promise 的状态和结果。看看下面的 demo:

let p= Promise.race([new Promise((resolve,reject)=>{
   setTimeout(()=>{
    resolve(1)
   })
}),new Promise((resolve,reject)=>{
    resolve(2)
})])
setTimeout(()=>{
    console.log(p)//promise:{promiseStatus:fulfilled,promiseResul:2}
})

我们可以尝试手写 Promise.race 的源码:

Promise.race = function (params) {
    return new Promise((resolve, reject) => {
        for (let i = 0; i < params.length; i++) {
            const item = params[i];
            Promise.resolve(item).then(
                (value) => {
                    resolve(value)
                }, (reason) => {
                    reject(reason)
                })
        }
    })
}

和上面两个方法类似,我们使用 Promise.resolve 将参数迭代器中非 promise 的参数变成 promise 统一处理。根据手写源码我们可以知道,then 方法帮我们做了两件事:

  • 暴露出了当前 Promise 实例的 result/reason
  • 展平了嵌套的 Promise 实例

Promise.any

Promise.any 方法和 Promise.all 方法是相反的:

  • all 方法是全对返回所有结果,错一个返回错误的
  • any 方法是全错返回错误的,对一个返回正确的 于是我们可以在 Promise.all 的手写源码基础上进行修改,得到下面代码:
Promise.any = function (params) {
    if (!(typeof params[Symbol.iterator] === 'function')) {
        throw new TypeError('params is not an iterator')
    }
    return new Promise((resolve, reject) => {
        //参数为空的时候,直接返回状态为 fulfilled 的 promise,result 为[]
        const final = []
        let len = args.length;
        if (len === 0) return resolve(final);
        for (let i = 0; i < params.length; i++) {
            const item = params[i];
            //遇到 resolve,直接改变结果 promise 的 promiseresult 和 promisestatus,后续的更改不会生效
            Promise.resolve(item).then((result) => {
                resolve(result);
            }, (reason) => {
                // 一旦有promise被拒绝就放入数组,攒满后才会返回
                final[i] = reason;
                if (--len === 0) {
                    reject(final);
                }

            });
        }
    });
};

错误处理 ❌

catchfinally 方法定义在 Promise 的原型上,可以被 Promise 实例继承,在 promise 的链式调用中十分重要。

Promise.prototype.catch

有一个经常被提及的概念就是异常传透

什么是异常传透?让我们通过 demo 来理解一下:

new Promise((resolve, reject) => {
    reject('我失败了~')
}).then(
    (value) => { console.log('onfulfilled1', value) },
).then(
    (value) => { console.log('onfulfilled2', value) },
).then(
    (value) => { console.log('onfulfilled3', value) },
).catch(
    (err) => { console.log('onRejected1', err) },//我失败了
)

异常传透说的是在写 promise 链式调用的时候,如果我们不主动写 onRejected 方法去捕获错误的话,错误会一直往下传递,直到被 catch 捕获,在学习如何手写 Promise 源码之后,我们可以知道,在 then 方法中,当 onRejected 方法为 undefined 的时候,会默认给给一个异常抛出的方法:

onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };

换句话说,我们表面上看到最后一个 catch 拿到了第一个 promise 的错误信息,实际上错误信息还是通过第一个 promise 逐个的往下进行的传递,于是我们可以想到,实际上这个 catch 方法等效于下面这种写法:

new Promise((resolve, reject) => {
    reject('我失败了~')
}).then(
    (value) => { console.log('onfulfilled1', value) },
).then(
    (value) => { console.log('onfulfilled2', value) },
).then(
    (value) => { console.log('onfulfilled3', value) },
).then(  
    (value)=>{ console.log('onfulfilled4', value)},
    (err) => { console.log('onRejected1', err) },//我失败了
)

所以我们可以尝试写一下 catch 方法的源码:

class Promise {
  ...

  catch (onRejected) {
    return this.then(null, onRejected);
  }
    ...
}

Promise.prototype.finally

finnally 方法返回一个 Promise 实例。在链式调用结束的时候,无论结果如何,都会执行这个函数。

我们可以通过一个 demo 来看看 finally 的基本使用:

let presolve = Promise.resolve('success')
let preject = Promise.reject('fail')
let p1 = presolve.finally(() => { })
let p2 = preject.finally(() => { })
setTimeout(function () {
    console.log(p1)//promise:{promiseStatus:fulfilled,promiseResult:'success'}
    console.log(p2)//promise:{promiseStatus:rejected,promiseResult:'fail'}
    console.log(p1 === presolve)//false
    console.log(p2 === preject)//false
})

从这个 demo 中我们可以看到 finally 方法的基本用法,除此之外还有两个小细节:

  • finlly 的返回值和调用 finally 方法的 Promise 实例的 promiseStatuspromiseResult 需要保持一致。
  • finlly 的返回值和调用 finally 方法的 Promise 实例不是同一个对象,返回的是新的 Promise 实例。 于是我们可以尝试写一下 Promise.prototype.finally 的源码:
class Promise {
   // ...  
   finally(onFinally) {
       return new Promise((resolve, reject) => {
           this.then((result) => {
               onFinally();
               resolve(result);
           }, (reason) => {
               onFinally();
               reject(reason);
           });
       });
   }
   // ...  
}

兼容性查询 ❓

以上所以的 Promise 的静态方法和错误处理 api 的兼容性可以通过下面这个网站进行查询:

Can I use?

小声 BB 🤡

又到了我最喜欢的小声BB环节,新年 🎏 flag 🎏 在持续稳步的推进中.

深圳疫情又严重了,前几天看到科兴的同行提着台式机跑毒差点笑出了声,笑完突然想到前几天猝死的吴姓同行,不禁又开始思考程序员这个岗位,转而到思考人生意义,但是仍旧没有思考出个所以然。

年前到做了一次年终体检,前几天到拿体检报告:

image.png

尿酸,血糖,血脂全部告警,可能还没等到被社会优化,自己就先把自己给优化了。

最近,在低调青年群也接触到了正念冥想,下载了一个相关 APP,开始尝试一下。

读书方面,在读鲁迅先生的《呐喊》,发现很多网友们玩的不亦乐乎的梗,加上历史背景的 buff 之后,其代表的意义竟是如此的沉重。

三月想继续把这个系列写一下,同时还想分享一下最近体验低代码平台的一些感受,还想分享一下学习 sketch 的收获,要分享的还有很多,to be continue。。。

🎉 🎉 觉得文章对您有帮助的小伙伴,请不要吝啬您的点赞~🎉 🎉

🎉 🎉 对文章中的措辞表达、知识点、文章格式等方面有任何疑问或者建议,请留下您的评论~🎉 🎉

参考文章 📁