资讯专栏INFORMATION COLUMN

实现一个符合标准的Promise

yuanzhanghu / 649人阅读

摘要:不同的的实现需要可以相互调用,搞清楚了标准之后,开始动手吧构造函数产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生实现也是使用的构造函数,因此我也决定使用构造函数的方法。

-- What i can"t create, i don"t understant

前言

实现Promise的目的是为了深入的理解Promies,以在项目中游刃有余的使用它。完整的代码见gitHub

Promise标准

Promise的标准有很多个版本,本文采用ES6原生Promise使用的Promise/A+标准。完整的Promise/A+标准见这里,总结如下:

promise具有状态state(status),状态分为pending, fulfilled(我比较喜欢叫做resolved), rejected。初始为pending,一旦状态改变,不能再更改为其它状态。当promise为fulfilled时,具有value;当promise为rejected时,具有reason;value和reason都是一旦确定,不能改变的。

promise具有then方法,注意了,只有then方法是必须的,其余常用的catch,race,all,resolve等等方法都不是必须的,其实这些方法都可以用then方便的实现。

不同的promise的实现需要可以相互调用

OK,搞清楚了promise标准之后,开始动手吧

Promise构造函数

产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生Promise实现也是使用的构造函数,因此我也决定使用构造函数的方法。

首先,先写一个大概的框架出来:

// 总所周知,Promise传入一个executor,有两个参数resolve, reject,用来改变promise的状态
function Promise(executor) {
    this.status = "pending"
    this.value = void 0 // 为了方便把value和reason合并

    const resolve = function() {}
    const reject = function() {} 
    executor(resolve, reject)
}

很明显,这个构造函数还有很多问题们一个一个来看

resolve和reject并没有什么卵用。

首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:

   function Promise(executor) {
       this.status = "pending"
       this.value = void 0 // 为了方便把value和reason合并

       const resolve = value => {
           this.value = value
           this.status = "resolved"
       }
       const reject = reason => {
           this.value = reason
           this.status = "rejected"
       } 
       executor(resolve, reject)
   }
   

然后,当resolve或者reject调用的时候,需要执行在then方法里传入的相应的函数(通知)。有没有觉得这个有点类似于事件(发布-订阅模式)呢?

   function Promise(executor) {
       this.status = "pending"
       this.value = void 0 // 为了方便把value和reason合并

       this.resolveListeners = []
       this.rejectListeners = []

       // 通知状态改变
       const notify(target, val) => {
           target === "resolved"
               ? this.resolveListeners.forEach(cb => cb(val))
               : this.rejectListeners.forEach(cb => cb(val))
       }

       const resolve = value => {
           this.value = value
           this.status = "resolved"
           notify("resolved", value)
       }
       const reject = reason => {
           this.value = reason
           this.status = "rejected"
           notify("rejected", reason)
       } 
       executor(resolve, reject)
   }

status和value并没有做到一旦确定,无法更改。这里有两个问题,一是返回的对象暴露了status和value属性,并且可以随意赋值;二是如果在executor里多次调用resolve或者reject,会使value更改多次。

第一个问题,如何实现只读属性:

   function Promise(executor) {
       if (typeof executor !== "function") {
           throw new Error("Promise executor must be fucntion")
       }

       let status = "pending" // 闭包形成私有属性
       let value = void 0

       ......

       // 使用status代替this.value
       const resolve = val => {
           value = val
           status = "resolved"
           notify("resolved", val)
       }
       const reject = reason => {
           value = reason
           status = "rejected"
           notify("rejected", reason)
       }

       // 通过getter和setter设置只读属性
       Object.defineProperty(this, "status", {
           get() {
               return status
           },
           set() {
               console.warn("status is read-only")
           }
       })

       Object.defineProperty(this, "value", {
           get() {
               return value
           },
           set() {
               console.warn("value is read-only")
           }
       })

第二个问题,避免多次调用resolve、reject时改变value,而且标准里(2.2.2.3 it must not be called more than once)也有规定,then注册的回调只能执行一次。

   const resolve = val => {
       if (status !== "pending") return // 避免多次运行
       value = val
       status = "resolved"
       notify("resolved", val)
   }

then注册的回调需要异步执行。

说到异步执行,对原生Promise有了解的同学都知道,then注册的回调在Micro-task中,并且调度策略是,Macro-task中执行一个任务,清空所有Micro-task的任务。简而言之,promise异步的优先级更高。

其实,标准只规定了promise回调需要异步执行,在一个“干净的”执行栈执行,并没有规定一定说要用micro-task,并且在低版本浏览器中,并没有micro-task队列。不过在各种promise的讨论中,由于原生Promise的实现,micro-task已经成成为了事实标准,而且promise回调在micro-task中也使得程序的行为更好预测。

在浏览器端,可以用MutationObserver实现Micro-task。本文利用setTimeout来简单实现异步。

   const resolve = val => {
       if (val instanceof Promise) {
           return val.then(resolve, reject)
       }

       // 异步执行
       setTimeout(() => {
           if (status !== "pending") return
           
           status = "resolved"
           value = val
           notify("resolved", val)
       }, 0)
   }

最后,加上错误处理,就得到了一个完整的Promise构造函数:

function Promise(executor) {
    if (typeof executor !== "function") {
        throw new Error("Promise executor must be fucntion")
    }

    let status = "pending"
    let value = void 0

    const notify = (target, val) => {
        target === "resolved"
            ? this.resolveListeners.forEach(cb => cb(val))
            : this.rejectListeners.forEach(cb => cb(val))
    }

    const resolve = val => {
        if (val instanceof Promise) {
            return val.then(resolve, reject)
        }

        setTimeout(() => {
            if (status !== "pending") return
            
            status = "resolved"
            value = val
            notify("resolved", val)
        }, 0)
    }

    const reject = reason => {
        setTimeout(() => {
            if (status !== "pending") return

            status = "rejected"
            value = reason
            notify("rejected", reason)
        }, 0)
    }

    this.resolveListeners = []
    this.rejectListeners = []

    Object.defineProperty(this, "status", {
        get() {
            return status
        },
        set() {
            console.warn("status is read-only")
        }
    })

    Object.defineProperty(this, "value", {
        get() {
            return value
        },
        set() {
            console.warn("value is read-only")
        }
    })

    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

总的来说,Promise构造函数其实只干了一件事:执行传入的executor,并构造了executor的两个参数。

实现then方法

首先需要确定的是,then方法是写在构造函数里还是写在原型里。
写在构造函数了里有一个比较大的好处:可以像处理status和value一样,通过闭包让resolveListeners和rejectListeners成为私有属性,避免通过this.rejectListeners来改变它。
写在构造函数里的缺点是,每一个promise对象都会有一个不同的then方法,这既浪费内存,又不合理。我的选择是写在原型里,为了保持和原生Promise有一样的结构和接口。

ok,还是先写一个大概的框架:

Promise.prototype.then = function (resCb, rejCb) {
    this.resolveListeners.push(resCb)
    this.rejectListeners.push(rejCb)

    return new Promise()
}

随后,一步一步的完善它:

then方法返回的promise需要根据resCb或rejCb的运行结果来确定状态。

Promise.prototype.then = function (resCb, rejCb) {
    return new Promise((res, rej) => {
        this.resolveListeners.push((val) => {
            try {
                const x = resCb(val)
                res(x) // 以resCb的返回值为value来resolve
            } catch (e) {
                rej(e) // 如果出错,返回的promise以异常为reason来reject
            }
        })

        this.rejectListeners.push((val) => {
            try {
                const x = rejCb(val)
                res(x) // 注意这里也是res而不是rej哦
            } catch (e) {
                rej(e) // 如果出错,返回的promise以异常为reason来reject
            }
        })
    })
}

ps:众所周知,promise可以链式调用,说起链式调用,我的第一个想法就是返回this就可以了,但是then方法不可以简单的返回this,而要返回一个新的promise对象。因为promise的状态一旦确定就不能更改,而then方法返回的promise的状态需要根据then回调的运行结果来决定。

如果resCb/rejCb返回一个promiseA,then返回的promise需要跟随(adopt)promiseA,也就是说,需要保持和promiseA一样的status和value。

this.resolveListeners.push((val) => {
    try {
        const x = resCb(val)

        if (x instanceof Promise) {
            x.then(res, rej) // adopt promise x
        } else {
            res(x)
        }
    } catch (e) {
        rej(e)
    }
})

this.rejectListeners.push((val) => {
    try {
        const x = resCb(val)

        if (x instanceof Promise) {
            x.then(res, rej) // adopt promise x
        } else {
            res(x)
        }
    } catch (e) {
        rej(e)
    }
})

如果then的参数不是函数,需要忽略它,类似于这种情况:

new Promise(rs => rs(5))
    .then()
    .then(console.log)

其实就是把value和状态往后传递

this.resolveListeners.push((val) => {
    if (typeof resCb !== "function") {
        res(val)
        return
    }

    try {
        const x = resCb(val)

        if (x instanceof Promise) {
            x.then(res, rej) // adopt promise x
        } else {
            res(x)
        }
    } catch (e) {
        rej(e)
    }
})

// rejectListeners也是相同的逻辑

如果调用then时, promise的状态已经确定,相应的回调直接运行

// 注意这里需要异步
if (status === "resolved") setTimeout(() => resolveCb(value), 0)
if (status === "rejected") setTimeout(() => rejectCb(value), 0)

最后,就得到了一个完整的then方法,总结一下,then方法干了两件事,一是注册了回调,二是返回一个新的promise对象。

// resolveCb和rejectCb是相同的逻辑,封装成一个函数
const thenCallBack = (cb, res, rej, target, val) => {
    if (typeof cb !== "function") {
        target === "resolve"
            ? res(val)
            : rej(val)
        return
    }

    try {
        const x = cb(val)

        if (x instanceof Promise) {
            x.then(res, rej) // adopt promise x
        } else {
            res(x)
        }
    } catch (e) {
        rej(e)
    }
}

Promise.prototype.then = function (resCb, rejCb) {
    const status = this.status
    const value = this.value
    let thenPromise

    thenPromise = new Promise((res, rej) => {
        /**
         * 这里不能使用bind来实现柯里画,规范里规定了:
         * 2.2.5: onFulfilled and onRejected must be called as functions (i.e. with no this value))
         */
        const resolveCb = val => {
            thenCallBack(resCb, res, rej, "resolve", val)
        } 
        const rejectCb = val => {
            thenCallBack(rejCb, res, rej, "reject", val)
        }

        if (status === "pending") {
            this.resolveListeners.push(resolveCb)
            this.rejectListeners.push(rejectCb)
        }

        if (status === "resolved") setTimeout(() => resolveCb(value), 0)
        if (status === "rejected") setTimeout(() => rejectCb(value), 0)
    })

    return thenPromise
}
不同的Promise实现可以互相调用

首先要明白的是什么叫互相调用,什么情况下会互相调用。之前实现then方法的时候,有一条规则是:如果then方法的回调返回一个promiseA。then返回的promise需要adopt这个promiseA,也就是说,需要处理这种情况:

new MyPromise(rs => rs(5))
    .then(val => {
        return Promise.resolve(5) // 原生Promise
    })
    .then(val => {
        return new Bluebird(r => r(5)) // Bluebird的promise
    })

关于这个,规范里定义了一个叫做The Promise Resolution Procedure的过程,我们需要做的就是把规范翻译一遍,并替代代码中判断promise的地方

const resolveThenable = (promise, x, resolve, reject) => {
    if (x === promise) {
        return reject(new TypeError("chain call found"))
    }

    if (x instanceof Promise) {
        return x.then(v => {
            resolveThenable(promise, v, resolve, reject)
        }, reject)
    }

    if (x === null || (typeof x !== "object" && typeof x !== "function")) {
        return resolve(x)
    }

    let called = false
    try {
        // 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
        const then = x.then

        if (typeof then !== "function") {
            return resolve(x)
        }

        then.call(x, v => {
            if (called) return
            called = true
            resolveThenable(promise, v, resolve, reject)
        }, r => {
            if (called) return
            called = true
            reject(r)
        })
    } catch (e) {
        if (called) return
        reject(e)
    }
}

到这里,一个符合标准的Promise就完成了,完整的代码如下:

function Promise(executor) {
    if (typeof executor !== "function") {
        throw new Error("Promise executor must be fucntion")
    }

    let status = "pending"
    let value = void 0

    const notify = (target, val) => {
        target === "resolved"
            ? this.resolveListeners.forEach(cb => cb(val))
            : this.rejectListeners.forEach(cb => cb(val))
    }

    const resolve = val => {
        if (val instanceof Promise) {
            return val.then(resolve, reject)
        }

        setTimeout(() => {
            if (status !== "pending") return
            
            status = "resolved"
            value = val
            notify("resolved", val)
        }, 0)
    }

    const reject = reason => {
        setTimeout(() => {
            if (status !== "pending") return

            status = "rejected"
            value = reason
            notify("rejected", reason)
        }, 0)
    }

    this.resolveListeners = []
    this.rejectListeners = []

    Object.defineProperty(this, "status", {
        get() {
            return status
        },
        set() {
            console.warn("status is read-only")
        }
    })

    Object.defineProperty(this, "value", {
        get() {
            return value
        },
        set() {
            console.warn("value is read-only")
        }
    })

    try {
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
}

const thenCallBack = (cb, res, rej, target, promise, val) => {
    if (typeof cb !== "function") {
        target === "resolve"
            ? res(val)
            : rej(val)
        return
    }

    try {
        const x = cb(val)
        resolveThenable(promise, x, res, rej)
    } catch (e) {
        rej(e)
    }
}

const resolveThenable = (promise, x, resolve, reject) => {
    if (x === promise) {
        return reject(new TypeError("chain call found"))
    }

    if (x instanceof Promise) {
        return x.then(v => {
            resolveThenable(promise, v, resolve, reject)
        }, reject)
    }

    if (x === null || (typeof x !== "object" && typeof x !== "function")) {
        return resolve(x)
    }

    let called = false
    try {
        // 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用
        const then = x.then

        if (typeof then !== "function") {
            return resolve(x)
        }

        then.call(x, v => {
            if (called) return
            called = true
            resolveThenable(promise, v, resolve, reject)
        }, r => {
            if (called) return
            called = true
            reject(r)
        })
    } catch (e) {
        if (called) return
        reject(e)
    }
}

Promise.prototype.then = function (resCb, rejCb) {
    const status = this.status
    const value = this.value
    let thenPromise

    thenPromise = new Promise((res, rej) => {
        const resolveCb = val => {
            thenCallBack(resCb, res, rej, "resolve", thenPromise, val)
        }
        const rejectCb = val => {
            thenCallBack(rejCb, res, rej, "reject", thenPromise, val)
        }

        if (status === "pending") {
            this.resolveListeners.push(resolveCb)
            this.rejectListeners.push(rejectCb)
        }

        if (status === "resolved") setTimeout(() => resolveCb(value), 0)
        if (status === "rejected") setTimeout(() => rejectCb(value), 0)
    })

    return thenPromise
}

测试脚本

关于promise的一些零散知识

Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。

promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。

感想与总结

实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97944.html

相关文章

  • 一步一步实现一个符合PromiseA+规范Promise库(1)

    摘要:今天我们来自己手写一个符合规范的库。是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。我们可以看到,其实就是一个构造函数。所以说我们的数组里存的是一个一个的的回调函数,也就是一个一个。 今天我们来自己手写一个符合PromiseA+规范的Promise库。大家是不是很激动呢?? showImg(https://segmentfault.com/img/bV6t4Z?...

    joyvw 评论0 收藏0
  • 【译】前端知识储备——Promise/A+规范

    摘要:在将来的其他规范中可能会涉及这些没有提及的内容。它禁止被触发多次。如果到了状态,那么所有的回调函数都必须按照他们原有的顺序进行调用执行。 概述 自从准备晋级之后,就拖更了很久了,既然晋级弄完了,那么也恢复更新了。 在面试别人的过程中,发现基本上没有人对整个Promise完全了解,因此希望通过这篇文章来帮助大家了解下Promise的全貌。本文的主要内容是Promise/A+规范的译文,主...

    Gemini 评论0 收藏0
  • 前端开发中Error以及异常捕获

    摘要:前端开发中的中的中,是一个构造函数,通过它创建一个错误对象。是核心对象,表示调用一个时发生的异常。将回调函数包裹一层接下来可以将统一进行处理。中的错误捕获在以前,可以使用来处理捕获的错误。研究结果在这里中的错误捕获的源码中,在关 本文首发于公众号:符合预期的CoyPan 写在前面 在前端项目中,由于JavaScript本身是一个弱类型语言,加上浏览器环境的复杂性,网络问题等等,很容易...

    Mr_houzi 评论0 收藏0
  • 传统 Ajax 已死,Fetch 永生

    摘要:结果证明,对于以上浏览器,在生产环境使用是可行的。后面可以跟对象,表示等待才会继续向下执行,如果被或抛出异常则会被外面的捕获。,,都是现在和未来解决异步的标准做法,可以完美搭配使用。这也是使用标准一大好处。只允许外部传入成功或失败后的回调。 showImg(https://cloud.githubusercontent.com/assets/948896/10188666/bc9a53...

    fai1017 评论0 收藏0
  • ES6: Promise

    摘要:换句话说该静态函数返回个处于状态的对象。等价于构造函数的静态函数,创建一个对象并以值作为参数调用句柄函数。等价于介绍构造函数的静态函数,参数是对象组成的可迭代数据集合。 一、概述 ES2015 Promise函数是对PromiseA+标准的实现,并且严格遵守该标准。 二、APIs 2.1 创建Promise对象 Promise构造函数的参数是个包含两个参数的函数,并且该函数的参数分别对...

    roadtogeek 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<