JS Promise实现原理

参考

https://www.bilibili.com/video/BV1x84y1g7ns/中有详细的视频讲解,下面是我的学习笔记。

Promise 初步认识

首先大致了解一下promise到底长什么样子,先定义下面这样一个request,封装一个promise函数用ajax从本地的data.js中获取数据。
在这里插入图片描述

打印出来可以看到,除了catch,then等几个函数之外,这个promise主要包含一个state属性和一个result。其实这里的PromiseResult就是promise中的resolve和reject返回的内容。但是这里为什么是undefined而不是resolve中的xhr.responseText呢,是因为同步api先执行了,次数还没有执行resolve。
在这里插入图片描述

Promise的设计思路

想要深入理解promise,必须要了解promise的实现。promise设计的思想是基于状态的,异步操作成功或失败仅仅会改变一个状态值,后续的所有操作都是基于这个状态进行的。这与回调函数不同,回调函数中当操作成功或失败之后往往直接执行了callback。下面手写一个简单的promise。

promise有以下几个特点:
1.首先这个promise传入了一个函数包括两个参数resolve和reject。
2.promise实现了对异步操作的状态管理。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
3.一旦状态改变,就冻结了,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
4.then函数要能接收两个回调函数来执行resolve和reject之后的业务。同时能获得promise执行后的resolve结果或reject结果。
5.then要能实现链式调用。

class Mypromise{
        constructor(executor) {  // 构造函数,这里的executor就是构建promise时的匿名函数
            executor(this.resolve,this.reject) //执行这个匿名函数,这个函数里面有两个回调函数,分别是resolve和reject。所以需要在下面定义这两个函数。
        }
        state = PENDING //promise的状态
        result = undefined // resolve的结果存在这里
        reason = undefined // reject的结果存在这里

        successCB = undefined
        failCB = undefined
        resolve = (result)=>{
            if(this.state !== PENDING) //一定要从PENDING -> FULLFILLED 如果已经FULLFILLED或者REJECTED 则不能再通过resolve和reject来改变状态了。
                return
            this.state = FULLFILLED
            this.result =  result // 如果需要传递resolve和reject的参数就要先存起来

            this.successCB && this.successCB(this.result) //意思是如果successCB不为undefine则执行他,执行语句与then中的正常执行是相同的。只不过是从then阶段移到了这里。

        }
        reject = (reason) =>{
            if(this.state !== PENDING)//一定要从PENDING -> FULLFILLED
                return
            this.state = REJECTED
            this.reason = reason

            this.failCB && this.failCB(this.reason)

        }

        then = (successCB,failCB) =>{ // then函数只要在状态确定了才会执行,并且分别在成功和失败两种情况下运行不同的回调函数。
            // 通常来说,前面的resolve和reject在封装promise时,会在异步api中使用,也就是说他们是异步执行的,这使得then函数一般情况下比resolve和reject要先执行。所以要处理状况为pending的情况。
            if(this.state === FULLFILLED)
                successCB(this.result) // 我们需要回调函数能获得resolve和reject传递的参数,因此我们在then中的回掉函数中传入实参。这样then函数中的回调函数就可以访问到result或者是reason。
            else if (this.state === REJECTED)
                failCB(this.reason)
            else if (this.state === PENDING) // 如果promise异步还没执行完,也就是说resolve和reject都没执行,状态还是pending。这时需要将本该执行的sucessCB或者failCB存起来,不在then阶段执行,而是交给resolve和reject执行。
            {
                this.successCB = successCB
                this.failCB = failCB
            }
        }
    }
// 下面是测试,发现已经可以实现基本的功能
let p = new Mypromise((resolve,reject) => {
        setTimeout(()=>{ // 模拟一个异步请求的情况
            resolve(123)
        })
    })

    p.then((result)=>{
        console.log(result)
    })

上面的代码已经可以实现基本的promise功能,但是还有一些缺陷。比如下面的情况,就会发现只打印了一次123。结合一下上面的源代码,异步的情况下,肯定是then函数先执行。在这个例子中,会先执行两次then函数,此时的then函数状态仍然是pending,所以会把本该执行的回调函数console.log(result)放到前面的resolve中执行,相当于是this.successCB = successCB执行了两次。

但是因为这段代码只有一个Mypromise实例,也就是只执行了一个resolve,就算你多次使用then函数,在上面的异步情况下,也只会打印一次,也就是在resolve里的那一次。因此为了这种需求,需要更改then函数中的pending情况。

let p = new Mypromise((resolve,reject) => {
        setTimeout(()=>{
            resolve(123)
        })
    })

    p.then((result)=>{
        console.log(result)
    })

    p.then((result)=>{
        console.log(result)
    })

这种初始为undefine,然后在then函数中赋值函数的方法不够用了。所以我们用一个数组来存,每次在then函数中没能执行,推迟到resolve或reject阶段的函数都存起来,最后在resolve和reject阶段把数组里的函数一起执行。

 class Mypromise{
        constructor(executor) {  // 构造函数,这里的executor就是构建promise时的匿名函数
            executor(this.resolve,this.reject) //执行这个匿名函数,这个函数里面有两个回调函数,分别是resolve和reject。所以需要在下面定义这两个函数。
        }
        state = PENDING //promise的状态
        result = undefined // resolve的结果存在这里
        reason = undefined // reject的结果存在这里

        successCB = []
        failCB = []
        resolve = (result)=>{
            if(this.state !== PENDING) //一定要从PENDING -> FULLFILLED 如果已经FULLFILLED或者REJECTED 则不能再通过resolve和reject来改变状态了。
                return
            this.state = FULLFILLED
            this.result =  result // 如果需要传递resolve和reject的参数就要先存起来

            while(this.successCB.length)
                this.successCB.shift()(this.result) // 数组中存的是函数,取出一个然后把result作为参数传进去。
        }
        reject = (reason) =>{
            if(this.state !== PENDING)//一定要从PENDING -> FULLFILLED
                return
            this.state = REJECTED
            this.reason = reason

            while(this.failCB.length)
                this.failCB.shift()(this.reason) // 数组中存的是函数,取出一个然后把reason作为参数传进去。

        }

        then = (successCB,failCB) =>{ // then函数只要在状态确定了才会执行,并且分别在成功和失败两种情况下运行不同的回调函数。
            // 通常来说,前面的resolve和reject在封装promise时,会在异步api中使用,也就是说他们是异步执行的,这使得then函数一般情况下比resolve和reject要先执行。所以要处理状况为pending的情况。
            if(this.state === FULLFILLED)
                successCB(this.result) // 我们需要回调函数能获得resolve和reject传递的参数,因此我们在then中的回掉函数中传入实参。这样then函数中的回调函数就可以访问到result或者是reason。
            else if (this.state === REJECTED)
                failCB(this.reason)
            else if (this.state === PENDING) // 如果promise异步还没执行完,也就是说resolve和reject都没执行,状态还是pending。这时需要将本该执行的sucessCB或者failCB存起来,不在then阶段执行,而是交给resolve和reject执行。
            {
                successCB && this.successCB.push(successCB) // 判空,不为空就放进数组
                failCB && this.failCB.push(failCB) // 防止一个promise的多个then调用,所以把任务放进数组中存起来,可以理解为一个队列,然后在resolve或reject阶段依次执行。
            }
        }
    }
    
 // 测试
    let p = new Mypromise((resolve,reject) => {
        setTimeout(()=>{
            resolve(123)
        })
    })

    p.then((result)=>{
        console.log(result)
    })

    p.then((result)=>{
        console.log(result)
    })

到这一步,仍然有一个问题没解决,那就是then的链式调用。现在的需求是,需要链式调用then的同时能访问上一个promise的resolve和reject的结果。思路就是我们的每个then函数返回的是一个全新的promise,这个返回的全新的promise中的resolve和reject就是上一个promise中的resolve和reject,就像是新瓶装旧酒,新的promise只放的resolve和reject都是之前一样的。这样一来,每个then函数代表的都是一个新的promise,每个promise都有then函数,这样一个链式结构就连起来了。你就可以一直.then.then.then了,同时每个then都能保存上一个promise的结果。
为此,我们可以改写then函数如下(其他的部分不变)

then = (successCB,failCB) =>{

            return new Mypromise((resolve,reject)=>{ // 思路是用新的瓶子(promise)来装之前promise的内容
                if(this.state === FULLFILLED) //这里的this指的是旧promise的的state 不是新的promise
                    //successCB(this.result)   这里本来是直接执行成功的回调函数。现在我们整理一下需求:1.这个函数需要执行  2.执行的结果需要传递给新的promise。
                    resolve(successCB(this.result)) // 我们直接执行这个函数,然后巧妙的把这个successCB函数的结果通过resolve存在新的promise中的result里。这里可以看上面的resolve函数的实现。
                else if (this.state === REJECTED)
                    reject(failCB(this.reason)) // 和上面一样,这个语句相当于执行failCB,同时把执行结果传给reject存到新的promise的reason里。
                else if (this.state === PENDING) // 如果promise异步还没执行完,也就是说resolve和reject都没执行,状态还是pending。这时需要将本该执行的sucessCB或者failCB存起来,不在then阶段执行,而是交给resolve和reject执行。
                {
                    //successCB && this.successCB.push(successCB)  这是之前的写法
                    successCB && this.successCB.push(()=>{
                        resolve(successCB(this.result))
                    }) // 同样我们要保证在异步情况下,新的promise中的then函数推迟到resolve和reject阶段再执行。
                    failCB && this.failCB.push(()=>{
                        reject(failCB(this.reason))
                    })  // 执行时我们需要把数组里所有successCB或者failCB的结果都用resolve或reject存到新promise的result和reason中。可能有点晕,反复琢磨一下就懂了。
                }
            })
        }

then函数中最后的pending阶段代码可能有点难以理解,只要对比之前的写法就会轻松很多。原来是把要做的回调函数存到数组里推迟执行,现在是把新瓶装旧酒这个过程,变成了旧酒存到数组里,推迟到resolve或reject阶段再一次性装到新瓶子里。

我们可以整理一下思路,根据上面这个代码,我们的then函数返回的是一个新的promise。我们可以想象三个角色:

  • 原来的promise -> promise1
  • 原来的promise的then函数 -> promise1.then
  • 新的promise -> promise2。

下面的例子是以promise中使用的异步api为前提的,也就是说then函数执行在resolve和reject前面。
promise1.then返回的就是promise2。 这个promise1.then创建了一个新的promise2.在promise1异步任务完成时,以成功为例,promise1的resovle会执行,他会通过promise2的resolve函数,将promise1的sucessCB的结果存在promise2的result中。也就是这个过程有一点点绕。然后我们得到的promise2状态一定和promise1相同(因为promise1执行resolve才会让promise2执行resolve,前者执行reject,后者才会执行reject),此外promise2的reason或result一定是promise的sucessCB或failCB的结果。

至此就实现了一个promise的主要特点。真正的Promise要复杂的多,还有一些其他功能比如race和all等等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值