Skip to content

🖊️ 手写Promise

⚡️ 状态和数据

​ 当Promise对象进入已决阶段时,表示异步操作结束,而该Promise对象的状态和数据代表了异步操作结果。我们可以在类中定义成员属性statusresult,分别用来表示当前Promise对象的状态和数据。而这两个成员是不能被外部直接获取的,因此需要将它们私有化。

​ Promise对象的状态总共有三种,并且它们的字符串表示都是不变的,即常量。因此,我们可以在类中将这三种状态定义成静态私有属性。

javascript
class Promise {
    static #PENDING = 'pending'
    static #FULFILLED = 'fulfilled'
    static #REJECTED = 'rejected'
    
    #status
    #result
}

🔄 resolve与reject

resolvereject两者都是用来改变当前Promise对象状态和数据的函数。在JavaScript中,普通函数中的this值是由该函数被调用时才确定的,而resolvereject两者通常会提供给外部使用。因此,我们不能确定函数体中的this值的具体指向。

​ ==那如何才能保证resolve和reject函数中的this值一定为当前Promise对象呢?==其实,我们可以使用箭头函数,箭头函数中的this值为定义时最近的外层上下文中的this值。因此,我们可以将resolvereject以箭头函数的形式定义成私有成员方法。

​ ==那为什么要将resolve和reject函数私有化呢?==resolvereject不能直接被外部使用,它们会在Promise实例化、executor执行时被当作实参传入。

resolvereject两者的逻辑类似,都是当当前Promise对象的状态为挂起时,改变其状态和数据。因此,我们可以将这个逻辑封装成一个函数。需要注意的是,封装的函数可以是普通函数,因为其只在resolvereject中被调用,this值是可以确定的。

javascript
class Promise {
    static #PENDING = 'pending'
    static #FULFILLED = 'fulfilled'
    static #REJECTED = 'rejected'

    #status
    #result
    
    #resolve = (data) => {
        this.#change(Promise.#FULFILLED, data)
    }

    #reject = (reason) => {
        this.#change(Promise.#REJECTED, reason)
    }

    #change(status, result) {
        if (this.#status !== Promise.#PENDING) return
        this.#status = status
        this.#result = result;
    }
}

🔨 构造器

​ Promise构造器接受一个executor函数,该函数会在Promise实例化时被同步执行,且会将resolvereject两者作为实参传入。但此过程仍需注意以下几点。

  1. 当构造器接受的executor不是函数类型时,会抛出一个TypeError错误。因此,我们可以写一个辅助函数来判断某个变量是否为函数类型。在JavaScript中,函数类型的判断有多种方式,这里选择typeof关键字。

  2. executor调用前,需将当前Promise实例的状态初始化为挂起。

  3. executor调用过程中发生错误,则拒绝当前Promise对象,原因为错误对象。

javascript
class Promise {
    static #PENDING = 'pending'
    static #FULFILLED = 'fulfilled'
    static #REJECTED = 'rejected'
    
    #status
    #result

    constructor(executor) {
        if (!this.#isFunction(executor)) {
            throw new TypeError('executor is not a function')
        }
        this.#status = Promise.#PENDING
        try {
            executor(this.#resolve, this.#reject)
        } catch (error) {
            this.#reject(error)
        }
    }

    #isFunction(func) {
        return typeof func === 'function'
    }

    #resolve = (data) => {
        this.#change(Promise.#FULFILLED, data)
    }

    #reject = (reason) => {
        this.#change(Promise.#REJECTED, reason)
    }

    #change(status, result) {
        if (this.#status !== Promise.#PENDING) return
        this.#status = status
        this.#result = result
    }
}

⛓️ then

then方法接受两个可选参数,一个是 onFulfilled(成功时的回调函数),一个是 onRejected(失败时的回调函数)。当Promise对象进入已决阶段时,其会根据自身状态,将关联的数据作为实参执行相应的回调。同时,then方法会返回一个全新的Promise对象,新Promise对象的状态和数据取决于回调的执行。

​ ==那可以在then方法中立即执行回调处理吗?==答案是不可以的,因为Promise对象的已决是可以发生在异步任务中的,例如定时器。在调用then方法时,当前Promise对象可能仍为挂起状态

​ ==那如何保证传入then方法的回调函数的执行时机是正确的呢?==我们可以使then方法不立刻执行相应的回调,而是将onFulfilledonRejected、新Promise对象的resolve、新Promise对象的reject四者作为对象的方法记录至一个数组handlers中。同时,将回调执行与新Promise对象的状态数据变更封装成一个函数#run,在then#change这两个方法的逻辑最后进行调用。

#run调用关系图

Q&A:

  1. ==为什么 handlers是一个数组?==

答:一个Promise对象可以多次注册回调函数。

then连续调用

  1. ==⭐️为什么需要记录新Promise对象的resolve和reject?==

答:新Promise对象的状态数据取决于后续任务的处理,即回调的执行。而状态和数据的变更必须依赖resolvereject。因此在记录回调外,还需额外记录它们。

  1. ==⭐️为什么#run方法需要被调用两次?==

答:若Promise对象的已决是同步的,则#run首先会在#change中被调用,而此时then未被调用,#handlers为空;若Promise对象的已决是异步的,则#run首先会在then中被调用,而此时#change未被调用,Promise对象的状态为挂起。因此,保证#run方法调用两次,并把以上这两种特殊情况排除,就能实现#run的真正逻辑只处理一次,且能够确保回调执行时机是正确的。在#run逻辑开头,可以使用以下代码作条件来排除这两种特殊情况。

#run排除特殊情况

​ ==确定了#run方法的调用时机,那其核心逻辑是什么呢?==其实,#run方法的任务非常简单与明确:执行回调,确定新Promise对象的状态和数据。

​ 新Promise对象的状态更改与回调函数的处理有关,而并不关心回调函数是用于处理成功还是失败。因此,我们可以继续封装一个#runCallback函数,而在#run方法中仅需选择onFulfilledonRejected中的一个作为回调即可。此时,#run方法的核心逻辑将转移至#runCallback中了。

​ ==那核心逻辑(新Promise对象的状态更改规则)究竟是什么呢?==具体来说,有以下几点。

  1. 传入的实参回调不是函数,则新Promise对象的状态和数据与前一个Promise对象保持一致。

  2. 后续任务处理成功,即回调执行无错误。新Promise对象的状态和数据取决于回调的返回值,以下是两种情况。

    • 回调的返回值也是一个任务(Promise对象),则新Promise对象的状态和数据与回调返回的Promise对象保持一致。

    • 回调的返回值不是一个任务(Promise对象),则新Promise对象的状态为已兑现,数据为返回值。

  3. 后续任务处理失败,即回调执行抛出错误,则新Promise对象的状态为已拒绝,数据为异常对象。

javascript
class Promise {
    static #PENDING = 'pending'
    static #FULFILLED = 'fulfilled'
    static #REJECTED = 'rejected'

    #status
    #result
    #handlers

    constructor(executor) {
        if (!this.#isFunction(executor)) {
            throw new TypeError('executor is not a function')
        }
        this.#status = Promise.#PENDING
        this.#handlers = []
        try {
            executor(this.#resolve, this.#reject)
        } catch (error) {
            this.#reject(error)
        }
    }

    #isFunction(func) {
        return typeof func === 'function'
    }

    #resolve = (data) => {
        this.#change(Promise.#FULFILLED, data)
    }

    #reject = (reason) => {
        this.#change(Promise.#REJECTED, reason)
    }

    #change(status, result) {
        if (this.#status !== Promise.#PENDING) return
        this.#status = status
        this.#result = result
        this.#run()
    }

    then(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            this.#handlers.push({onFulfilled, onRejected, resolve, reject})
            this.#run()
        })
    }

    #run() {
        // ⭐难点①
        if (this.#handlers.length === 0 || this.#status === Promise.#PENDING) return
        while (this.#handlers.length) {
            const {onFulfilled, onRejected, resolve, reject} = this.#handlers.shift()
            if (this.#status === Promise.#FULFILLED) {
                this.#runCallback(onFulfilled, resolve, reject)
            } else {
                this.#runCallback(onRejected, resolve, reject)
            }
        }
    }

    #runCallback(callback, resolve, reject) {
        if (!this.#isFunction(callback)) {
            const settled = this.#status === Promise.#FULFILLED ? resolve : reject
            settled(this.#result)
            return
        }
        try {
            const result = callback(this.#result)
            if (result instanceof Promise) {
                // ⭐难点②
                result.then(resolve, reject)
            } else {
                resolve(result)
            }
        } catch (error) {
            reject(error)
        }
    }
}

tip: then方法是Promise最核心的设计,其实现共有两个难点。

  1. ⭐回调的调用时机。解决方式上文已讲述。
  2. ⭐当回调成功执行且返回一个Promise对象后,新Promise对象的状态数据应与回调返回的Promise对象保持一致。解决方式为:调用返回Promise对象的then方法,并将新Promise对象的resolvereject分别注册为成功回调和失败回调。

🛠️ 微任务

​ Promise的回调为微任务。实际上,截至目前为止,ECMAScript标准中并没有微任务的概念。微任务是JS任务调度的实现机制,目前很多主流的JS运行环境都采用这种机制来实现事件循环,例如大多数现代浏览器、Node.js。

​ 需要注意的是,不同的 JavaScript 运行环境或引擎实现微任务的方式是有差异的。在现代浏览器环境中,我们可以利用MutationObserver API来创建微任务;在Node.js环境中,我们可以利用process.nextTick()来创建微任务。

tip: 在Node.js环境中,任务队列是分种类的,并有各自的优先级。微任务队列分为nextTick队列Promise队列。需要注意的是,这个Promise是Node.js中内置的构造函数,并非我们手写的Promise。而nextTick队列的执行优先级高于Promise队列,因此,我们只是借助了nextTick模拟了一个微任务。

javascript
class Promise {
    static #PENDING = 'pending'
    static #FULFILLED = 'fulfilled'
    static #REJECTED = 'rejected'

    #status
    #result
    #handlers

    constructor(executor) {
        if (!this.#isFunction(executor)) {
            throw new TypeError('executor is not a function')
        }
        this.#status = Promise.#PENDING
        this.#handlers = []
        try {
            executor(this.#resolve, this.#reject)
        } catch (error) {
            this.#reject(error)
        }
    }

    #isFunction(func) {
        return typeof func === 'function'
    }

    #resolve = (data) => {
        this.#change(Promise.#FULFILLED, data)
    }

    #reject = (reason) => {
        this.#change(Promise.#REJECTED, reason)
    }

    #change(status, result) {
        if (this.#status !== Promise.#PENDING) return
        this.#status = status
        this.#result = result
        this.#run()
    }

    then(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            this.#handlers.push({onFulfilled, onRejected, resolve, reject})
            this.#run()
        })
    }

    #run() {
        if (this.#handlers.length === 0 || this.#status === Promise.#PENDING) return
        while (this.#handlers.length) {
            const {onFulfilled, onRejected, resolve, reject} = this.#handlers.shift()
            if (this.#status === Promise.#FULFILLED) {
                this.#runCallback(onFulfilled, resolve, reject)
            } else {
                this.#runCallback(onRejected, resolve, reject)
            }
        }
    }

    #runCallback(callback, resolve, reject) {
        this.#runMicroTask(() => {
            if (!this.#isFunction(callback)) {
                const settled = this.#status === Promise.#FULFILLED ? resolve : reject
                settled(this.#result)
                return
            }
            try {
                const result = callback(this.#result)
                if (result instanceof Promise) {
                    result.then(resolve, reject)
                } else {
                    resolve(result)
                }
            } catch (error) {
                reject(error)
            }
        })
    }

    #runMicroTask(task) {
        if (!this.#isFunction(task)) throw new Error("task is not a function")
        if (typeof MutationObserver === "function" &&
            typeof MutationObserver.prototype.observe === "function") {
            const observer = new MutationObserver(task)
            const textNode = document.createTextNode("origin")
            observer.observe(textNode, {
                characterData: true
            })
            textNode.data = 'change'
        } else if (typeof process === 'object' && typeof process.nextTick === "function") {
            process.nextTick(task)
        } else (
            setTimeout(task, 0)
        )
    }
}

📜 Promise A+规范

​ Promise A+规范是一个社区规范,定义了Promise的行为、方法和交互方式,以确保在不同的JavaScript环境中Promise的一致性和可互操作性。

​ 一致性与可互操作性是指符合Promise A+规范的Promise对象可以与其他符合同样规范的Promise对象进行无缝交互和组合,而不仅限于JavaScript运行环境内置的Promise。

​ 综上所述,我们不能直接通过result instanceof Promise 来判断回调返回值是否是一个任务对象,而应该封装一个函数来判断返回值的类型是否符合Promise A+规范,以下为判断条件。

  1. 类型为functionobject
  2. 类型需实现Thenable,即实例存在一个then方法。

tip:

  1. async/await也支持与其他遵循 Promise A+ 规范的类型进行交互。
  2. async虽然支持兼容交互,但是它永远返回环境内置的Promise类型的对象。
javascript
class Promise {
    static #PENDING = 'pending'
    static #FULFILLED = 'fulfilled'
    static #REJECTED = 'rejected'

    #status
    #result
    #handlers

    constructor(executor) {
        if (!this.#isFunction(executor)) {
            throw new TypeError('executor is not a function')
        }
        this.#status = Promise.#PENDING
        this.#handlers = []
        try {
            executor(this.#resolve, this.#reject)
        } catch (error) {
            this.#reject(error)
        }
    }

    #isFunction(func) {
        return typeof func === 'function'
    }

    #resolve = (data) => {
        this.#change(Promise.#FULFILLED, data)
    }

    #reject = (reason) => {
        this.#change(Promise.#REJECTED, reason)
    }

    #change(status, result) {
        if (this.#status !== Promise.#PENDING) return
        this.#status = status
        this.#result = result
        this.#run()
    }

    then(onFulfilled, onRejected) {
        return new Promise((resolve, reject) => {
            this.#handlers.push({onFulfilled, onRejected, resolve, reject})
            this.#run()
        })
    }

    #run() {
        if (this.#handlers.length === 0 || this.#status === Promise.#PENDING) return
        while (this.#handlers.length) {
            const {onFulfilled, onRejected, resolve, reject} = this.#handlers.shift()
            if (this.#status === Promise.#FULFILLED) {
                this.#runCallback(onFulfilled, resolve, reject)
            } else {
                this.#runCallback(onRejected, resolve, reject)
            }
        }
    }

    #runCallback(callback, resolve, reject) {
        this.#runMicroTask(() => {
            if (!this.#isFunction(callback)) {
                const settled = this.#status === Promise.#FULFILLED ? resolve : reject
                settled(this.#result)
                return
            }
            try {
                const result = callback(this.#result)
                if (this.#isPromiseAPlus(result)) {
                    result.then(resolve, reject)
                } else {
                    resolve(result)
                }
            } catch (error) {
                reject(error)
            }
        })
    }

    #runMicroTask(task) {
        if (!this.#isFunction(task)) throw new Error("task is not a function")
        if (typeof MutationObserver === "function" &&
            typeof MutationObserver.prototype.observe === "function") {
            const observer = new MutationObserver(task)
            const textNode = document.createTextNode("origin")
            observer.observe(textNode, {
                characterData: true
            })
            textNode.data = 'change'
        } else if (typeof process === 'object' && typeof process.nextTick === "function") {
            process.nextTick(task)
        } else (
            setTimeout(task, 0)
        )
    }

    #isPromiseAPlus(value) {
        return !(value == null || (typeof value !== 'object'
            && typeof value !== 'function') || typeof value.then !== 'function')
    }
}

🔌 静态API

Promise.resolve

Promise.resolve用于返回一个已兑现的Promise对象,数据为给定的第一个参数。此过程有以下注意点。

  1. 若参数类型为内置的Promise,则直接返回
  2. 若参数类型不是内置的Promise,但符合Promise A+规范,则返回与其状态数据保持一致的nei'zh
javascript
  1. reject

  2. all

  3. any

  4. allSettled

  5. race

上次更新于: