逐步实现一个Promise

318 阅读12分钟

基本功能实现

Promise 基本使用方式

相信大家都知道 Promise 的基本用法,构造 Promise 实例时,需要接受一个函数作为 executor,在 constructor 内部立即执行。then 方法接收成功和失败之后执行的函数。

let promise = new Promise((resolve, reject) => { //这个传入的函数叫做 executor 执行者,会立刻执行
    throw new Error('失败') //抛出错误也算失败
    //resolve('成功了')
    //reject('失败了') //被屏蔽
}).then((value) => {
    console.log(value)
}, (err) => {
    console.log(err)
})

基本功能要求

  1. 三个状态:等待态(默认)、成功态、失败态 ,一旦成功或者失败就不会变化。
  2. executor 接受两个函数作为参数:resolve代表成功,reject代表失败,这两个函数是在 Promise 构造函数内部实现并作为参数传入executor 的。
  3. then 方法接受成功和失败之后执行的函数。
  4. executor 执行的时候抛出错误也算失败

代码实现

const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

class Promise {
    constructor(executor) {
        this.status = PENDING //默认是Pending
        this.value = undefined //成功值
        this.reason = undefined //失败原因
        //下面是两个回调函数
        //成功函数
        let resolve = (value) => {
            //只有pending是才能修改状态,这样就保证只会被修改一次了
            if (this.status === PENDING) {
                this.value = value
                this.status = FULFILLED
            }
        }
        //失败函数
        let reject = (reason) => {
            //只有pending时才能修改状态
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
            }
        }
        //executor执行错误直接reject
        try {
            executor(resolve, reject) //默认executor立刻执行
        } catch (err) {
            reject(err) //如果执行时发生错误,会使得
        }
    }
    //then方法
    then(onfulfilled, onrejected) {
        if (this.status === FULFILLED) {
            onfulfilled(this.value)
        } else if (this.status === REJECTED) {
            onrejected(this.reason)
        }
        // return new Promise() 待实现
    }
}

发布订阅:解决异步问题

我们都知道 Promise 是用来解决异步问题的,上面实现的代码中,如果 resolve 是在 seTimeout后执行的,如果我们直接调用了 then 方法,实例的状态是 pending,这时该怎么办?

这时,我们可以用到发布订阅,只要是 Promise 实例处于 pending 状态时,就把 then 方法中的成功和失败函数进行订阅,即分别保存,只要状态发生了变化就发布,这些回调函数(可能多次调用了then,所以就有很多函数)就会被执行。

如果只是简单的将函数保存在数组里面,那么这些回调函数在后面执行的时候如果还需要传入参数或者进行其他的一些操作呢,我们先暂时将保存到数组的函数再封装一层函数,后续再完善。

class Promise {
    constructor(executor) {
        this.status = PENDING //默认是Pending
        this.value = undefined //成功值
        this.reason = undefined //失败原因
        this.onRejectedCallbacks = [] //成功的回调数组
        this.onFulfilledCallbacks = [] //失败的回调数组
        //下面是两个回调函数?
        //成功函数
        let resolve = (value) => {
            //只有pending是才能修改状态
            if (this.status === PENDING) {
                this.value = value
                this.status = FULFILLED
                this.onFulfilledCallbacks.forEach(cb => cb()) //发布
            }
        }
        //失败函数
        let reject = (reason) => {
            //只有pending时才能修改状态
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onRejectedCallbacks.forEach(cb => cb()) //发布
            }
        }
        //executor执行错误也是直接reject
        try {
            executor(resolve, reject) //默认executor立刻执行
        } catch (err) {
            reject(err) //如果执行时发生错误,会使得
        }
    }
    then(onfulfilled, onrejected) {
        //前两个条件同步的时候就执行
        if (this.status === FULFILLED) {
            onfulfilled(this.value)
        } else if (this.status === REJECTED) {
            onrejected(this.reason)
        } else {
            //如果是异步就先订阅好
            this.onFulfilledCallbacks.push(() => {
                onfulfilled(this.value)
            })
            this.onRejectedCallbacks.push(() => {
                onrejected(this.reason)
            })
        }
        // return new Promise()
    }
}

完善 then 方法

我们知道 then 是会返回一个新的 Promise 实例的,接下来,我们就来讲,如何返回有一个 Promise 实例,并且符合 Promise 的使用规范。

then 方法基本特点

再说明一下链式使用 then 的特点:

  1. 如果一个 Promise 的 then 方法中的函数(成功和失败)返回的结果是一个 Promise 的话,会自动将这个Promise 执行,采用这个返回的 Promise 的状态,并将状态对应的结果向后面的传递;
  2. 如果 then 方法中的函数(成功和失败)返回的结果是 undefined 或其他普通值,那么会将这个普通值作为下一次的成功结果。
  3. 只有两种情况会失败,要么返回一个失败的 Promise ,要么抛出错误,也就是说话如果 onrejected() 函数只要返回的不是错误的情况,也会让它的返回值作为新的 Promise 的成功值。

显然,需要在 then 方法中判断两个成功和失败返回的结果类型,再利用其构造新的 Promise 实例进行返回。

返回 Promise 实例,记为 promise2,其 revolse 和 rejcet 的调用是受到当前 Promise 中 then 的 onfulfilled()、onrejected() 这两个函数的返回值影响,如何将其传入 promise2 的 executor 中呢?

这里我们需要用到闭包,我们需要让 executor 执行的时候依然使用当前 Promise 实例中的一些变量。 这里有两种方式,一种是将 onfulfilled()、onrejected() 的返回值在构造 promise2 时使用,另一种比较巧妙的方法是利用 executor 立即执行的特性,直接将 onfulfilled()、onrejected() 的运行写在 executor 函数中,我们使用后者。

代码实现

then(onfulfilled, onrejected) {
    let promise2 = new Promise((resolve, reject) => { //这里的再传给构造函数,立即执行,其实是个闭包,使用箭头函数,使得 this 是现在的这个 Promise,参数都是当前这个 Promise 的, 有点绕!
        //前两个条件同步的时候就执行
        if (this.status === FULFILLED) {
            let x = onfulfilled(this.value)
            resolve(x)//还有问题,需要类型判断等,其他地方的 x 也需要处理,接下来完善,
        } else if (this.status === REJECTED) {
            let x = onrejected(this.reason)
        } else {
            //如果是异步就先订阅好,同一个promise被多次then
            this.onFulfilledCallbacks.push(() => {
                let x = onfulfilled(this.value)
            })
            this.onRejectedCallbacks.push(() => {
                let x = onrejected(this.reason)
            })
        }
    })
    return promise2
}

题外话:其实这里的 resolve() 我一开始会产生疑问,它的执行到底是影响当前的 Promise 还是 promise2,其实我们把它定义成了箭头函数,箭头函数内部的 this 其实可以用闭包去理解,定义在哪,就已经确定了这个this就是定义地方的外面那个this,这就是闭包!!!!所以影响的是 promise2。

使用 resolvePromise 带来新的问题

接下来再实现判断返回的类型,我们按照规范来创建一个公共函数 resolvePromise,来执行这样一个操作。 先给出其输入参数,公共函数 resolvePromise

const resolvePromise = (promise2, x, resolve, reject) => {
    console.log(promise2)
}

在 then 中调用 resolvePromise

then(onfulfilled, onrejected) {
    let promise2 = new Promise((resolve, reject) => { 
        if (this.status === FULFILLED) {
            let x = onfulfilled(this.value)
            try {
                resolvePromise(promise2, x, resolve, reject) 
            } catch (err) {
                console.log(err)
            }
        } else if (this.status === REJECTED) {
            let x = onrejected(this.reason)
            try {
                resolvePromise(promise2, x, resolve, reject) 
            } catch (err) {
                console.log(err)
            }
        } else {
            this.onFulfilledCallbacks.push(() => {
                let x = onfulfilled(this.value)
                try {
                    resolvePromise(promise2, x, resolve, reject) 
                } catch (err) {
                    console.log(err)
                }
            })
            this.onRejectedCallbacks.push(() => {
                let x = onrejected(this.reason)
                try {
                    resolvePromise(promise2, x, resolve, reject) 
                } catch (err) {
                    console.log(err)
                }
            })
        }
    })
    return promise2
}

这样写是有问题的,这里特意在 resolvePromise 执行的时候进行错误捕获。我们尝试下在 resolvePromise 中打印一下 promsie2,发现会报错:ReferenceError: promise2 is not defined,因为现在的写法,在执行 resolvePromise 的时候,promise2 还没被构造完,就要用到它是不可能的。这里我们要用到异步的方式来实现,这里选用 setTimeout,这样可以使得 resolvePromise 函数执行的时候 promise2 已经被 new 完成了。

then(onfulfilled, onrejected) {
    let promise2 = new Promise((resolve, reject) => {
        if (this.status === FULFILLED) {
            setTimeout(() => {
                let x = onfulfilled(this.value)
                resolvePromise(promise2, x, resolve, reject) 
            }, 0)
        } else if (this.status === REJECTED) {
            setTimeout(() => {
                let x = onrejected(this.reason)
                resolvePromise(promise2, x, resolve, reject)
            }, 0)
        } else {
            this.onFulfilledCallbacks.push(() => {
                setTimeout(() => {
                    let x = onfulfilled(this.value)
                    resolvePromise(promise2, x, resolve, reject)
                }, 0)
            })
            this.onRejectedCallbacks.push(() => {
                setTimeout(() => {
                    let x = onrejected(this.reason)
                    resolvePromise(promise2, x, resolve, reject)
                }, 0)
            })
        }
    })
    return promise2
}

在 executor 引入异步的执行方式,又带来了新的问题,如果 onfulfilled 或者 onrejected 执行过程中报错了,比如

let Promise = require('./promise')
let p = new Promise((resolve, reject) => {
    resolve(1)
})
p.then(data => {
    throw new Error('222')
    console.log(data)
    return data
}, err => {
    console.log(err)
}).then(data=>{
	console.log(data)
})

不会被 executor 执行处的 try-catch 捕获,因此不能修改 Promise 的状态,会直接报错,Promise构造失败。 图1 我们现在要捕获这个错误,使得新的 Promise 实例可以成功构造。显然,只需要在 onfulfilled 和 onrejected 执行时也 try-catch 即可。

then(onfulfilled, onrejected) {
    let promise2 = new Promise((resolve, reject) => { 
        if (this.status === FULFILLED) {
            console.log(1)
            setTimeout(() => {
                try {
                    let x = onfulfilled(this.value)
                    resolvePromise(promise2, x, resolve, reject) 
                } catch (err) {
                    console.log(err)
                    reject()
                }
            }, 0)
        } else if (this.status === REJECTED) {
            setTimeout(() => {
                try {
                    let x = onrejected(this.reason)
                    resolvePromise(promise2, x, resolve, reject)
                } catch (err) {
                    reject(err)
                }
            }, 0)
        } else {
            this.onFulfilledCallbacks.push(() => {
                setTimeout(() => {
                    try {
                        let x = onfulfilled(this.value)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (err) {
                        reject(err)
                    }
                }, 0)
            })
            this.onRejectedCallbacks.push(() => {
                setTimeout(() => {
                    try {
                        let x = onrejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (err) {
                        reject(err)
                    }
                }, 0)
            })
        }
    })
    return promise2
}

then 的可选参数

then 如果不接收函数参数,会发生值穿透,也就前一个 Promise 的 onFulfilled 或者 onRejected 接收的参数直接换递给下一个 Promise 的 resolve 或者 reject。比如下面的例子,依然会打印出 1。

//可选参数
let p = new Promise((resolve, reject) => {
    resolve(1)
})
p.then().then(data => {
    console.log(data)
})

上面的代码相当于下面的代码,即第一个 then 的 onFulfilled 函数其实是直接返回成功值,将其构造为新的返回的 Promise 的成功值,这样就完成了值的穿透传递。

//可选参数
let p = new Promise((resolve, reject) => {
    resolve(1)
})
p.then(data => data).then(data => {
    console.log(data)
})

reject 的值穿透同理,但是不同的是,出现的错误需要被抛出,否则会变成成功值!

let p3 = new Promise((resolve, reject) => {
    reject(1)
})
p3.then(data => data, err => err).then(data => {
    console.log(2)
}, err => {
    console.log(err)
})

没错,上面的代码打印出的是 2 ! 应该改成抛出错误的形式:

let p3 = new Promise((resolve, reject) => {
    reject(1)
})
p3.then(data => data, err => {
    throw err
}).then(data => {
    console.log(2)
}, err => {
    console.log(err)
})

在我们实现的 then 方法的最开始加上下面代码即可

onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : data => data
onrejected = typeof onrejected === 'function' ? onrejected : err => {
    throw err
}

resolvePromise:解析 Promise

再接着写 resolvePromise,这里只讲一下返回的 x 是 Promise 的情况,其他的直接看代码注释 我们需要对返回的 Promise x 执行 then方法,在 onfulfilled 内 resolve,在onrejected 内 reject,这样就实现了在 x 成功的是否使 promise2 执行 resolve,失败的时候执行 reject,同时也获得了成功和失败值,妙啊!

const resolvePromise = (promise2, x, resolve, reject) => {
    // 判断 x 的值
    //按照规范写,所有的 Promise 都遵循这个规范,写法必须兼容所有 Promise
    // 如果 promsie2 和 x 是同一个对象,就报错
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
    }
    if (typeof x === 'object' && x !== null || typeof x === 'function') {
        try {
            let then = x.then
            if (typeof then === 'function') { //当前有then方法,姑且认为 x 是个Promise
                //注意,是返回结果中的 Promise,我们需要对其立即执行 then,才能知道到底是成功的才是失败的,
                //同时也可以得到成功和失败的值,也就是 promise2 的状态由这个返回的 Promise 决定
                then.call(x, y => {
                    resolve(y) //采用成功结果
                }, r => {
                    reject(r) //采用失败结果
                }) //就是x.then
            } else {
                //普通对象或函数
                resolve(x) //直接成功即可
            }
        } catch (err) {
            reject(err)
        }
    } else {
        //x 是普通值
        resolve(x) //直接让 promise2 成功即可
    }
}

这里再提一个神奇的事情,我们如果让then中的方法返回的不仅是个 Promise,它 resolve 接收的参数也是一个 Promise。

let p = new Promise((resolve, reject) => {
    resolve(1)
})
let promise2 = p.then(data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // resolve('hello')
            resolve(new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve('hello')
                }, 1000)
            }))
        }, 1000)
    })
}, err => {
    console.log(err)
})
promise2.then(data => {
    console.log(data)
})

我们使用默认的 Promise 来执行,得到 使用我们自己目前写的 Promise 来执行,得到 说明 resolve 接收的 Promise 也应该执行了then,我们需要再对这个 Promise进行解析,其实就是递归调用,直到不是 Promise 为止,不过一般也没人这么写吧。

我现在比较好奇 reject 接收的参数也是一个 Promise 会发生什么。

let p = new Promise((resolve, reject) => {
    resolve(1)
})
let promise2 = p.then(data => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // resolve('hello')
            reject(new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve('hello')
                }, 1000)
            }))
        }, 1000)
    })
}, err => {
    console.log(err)
})
promise2.then(data => {
    console.log(data)
}, err => {
    console.log(err)
})

我们使用默认的 Promise 来执行,得到 可以发现 reject 的 Promise 没有被解析,也对,都报错了,还解析个毛线啊! 那么我们只需要在 resolve 处修改一下就行,只需要对前面的版本改一行代码。

const resolvePromise = (promise2, x, resolve, reject) => {
    // 判断 x 的值
    //按照规范写,所有的 Promise 都遵循这个规范,写法必须兼容所有 Promise
    // 如果 promsie2 和 x 是同一个对象,就报错
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
    }
    if (typeof x === 'object' && x !== null || typeof x === 'function') {
        try {
            let then = x.then
            if (typeof then === 'function') { //当前有then方法,姑且认为 x 是个Promise
                //注意,是返回结果中的 Promise,我们需要对其立即执行 then,才能知道到底是成功的才是失败的,
                //同时也可以得到成功和失败的值,也就是 promise2 的状态由这个返回的 Promise 决定
                then.call(x, y => {
                    resolvePromise(promise2, y, resolve, reject)  //递归解析,最终会落到其他分支
                }, r => {
                    reject(r) //采用失败结果
                }) //就是x.then
            } else {
                //普通对象或函数
                resolve(x) //直接成功即可
            }
        } catch (err) {
            reject(err)
        }
    } else {
        //x 是普通值
        resolve(x) //直接让 promise2 成功即可
    }
}

防止多次调用成功和失败函数,这部分我们写的代码是不需要的,为了通过测试!其实就是设置一个变量,只要置为真,就不能再被执行。

const resolvePromise = (promise2, x, resolve, reject) => {
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
    }
    if (typeof x === 'object' && x !== null || typeof x === 'function') {
        let called //防止多次调用
        try {
            let then = x.then
            if (typeof then === 'function') { 
                then.call(x, y => {
                    if (called) return
                    called = true
                    resolvePromise(promise2, y, resolve, reject) //递归解析,最终会落到起它分支
                }, r => {
                    if (called) return
                    called = true
                    reject(r) //采用失败结果
                }) 
            } else {
                //普通对象或函数
                resolve(x) //直接成功即可
            }
        } catch (err) {
            if (called) return
            called = true
            reject(err)
        }
    } else {
        //x 是普通值
        resolve(x) 
    }
}

defer 对象

把 Promise 实例本身和它的 resovle 以及 reject 方法挂载defer对象上,可以解决封装嵌套问题。

//延迟对象/函数
Promise.defer = Promise.deferred = function () {
    let dfd = {}
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

以一个读取文件函数为例看看是否使用 defer 的区别,我目前没看出有什么本质的区别:

let Promise = require('./promise')

let fs = require('fs')

function read(url) {
	//1)把异步操作写在executor中
    // return new Promise((resolve, reject) => {
    //     fs.readFile(url, 'utf8', function (err, data) {
    //         if (err) reject(err)
    //         resolve(data)
    //     })
    // })
    
    //2)Promise.defer(),可以解决封装嵌套问题
    let dfd = Promise.defer()
    fs.readFile(url, 'utf8', function (err, data) {
        if (err) dfd.reject(err)
        dfd.resolve(data)
    })
    return dfd.promise
}

read('./name.txt').then(data => {
    console.log(data)
})

测试

使用 promises-aplus-tests 进行测试,完美通过。 至此,一个具有基本功能的 Promise 已经完成。

完整代码链接。