系列文章目录
根据PromiseA+规范,自己手动实现一个Promise
前言
在上一篇文章中,我们已经实现了MyPromise构造器部分的相关代码,本篇文章则着重实现then方法的回调执行时机
一、then方法
根据promise A+规范中的描述总结,大致归纳为下述内容
- onFulfilled(可选):一个在此 Promise 对象被兑现时异步执行的函数。它的返回值将成为 then() 返回的Promise对象的兑现值。此函数被调用时将传入参数value(代表Promise 对象的兑现值)。
- onRejected(可选):一个在此Promise 对象被拒绝时异步执行的函数。它的返回值将成为 catch() 返回的 Promise对象的兑现值。此函数被调用时将传入参数reason(表示Promise 对象被拒绝的原因)。
- 返回值:立即返回一个新的 Promise 对象。
二、开始手动实现
思路:
首先,创建一个then方法,它接收两个函数作为参数,分别是onFulfilled和onRejected,之后返回一个MyPromise实例,在实例中,通过判断此时MyPromise的状态是成功态(resolve)还是失败态(reject)来决定具体是执行onFulfilled还是onRejected,无论执行哪个回调,都会将当前promise的结果作为参数传进去。
根据上述思路可以完成下面的代码
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
#state = PENDING
#result = undefined
constructor(executor) {
const resolve = (data) => {
this.#changeState(FULFILLED, data)
}
const reject = (reason) => {
this.#changeState(REJECTED, reason)
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
#changeState(state, result) {
if (this.#state !== PENDING) return
this.#state = state
this.#result = result
}
then(onFulfilled, onRejected) { // onFulfilled和onRejected是外部通过then方法传入的成功与失败对应的执行函数
return new MyPromise((resolve, reject) => { // resolve与reject是当前返回的MyPromise实例的成功与失败回调
if (this.#state === FULFILLED) { // 如果是成功,执行onFulfilled函数
onFulfilled(this.#result)
} else if (this.#state === REJECTED) // 如果是失败,执行onRejected函数
onRejected(this.#result)
})
}
}
根据上述思路执行一下代码,会发现控制台能够正常输出的。(状态是pending没变化是正常的,因为还没写到修改状态的步骤)
但如果此时是异步传入结果呢?
通过一个setTimeout将resolve包裹起来就会发现,控制台没有输出了,这是因为是此时是异步传入结果,所以当知道可以具体执行哪个回调函数时,then方法的执行早就已经结束了,自然也就没办法再次执行具体的回调函数了。
解决思路:
要解决上面的问题也很简单,就是在then方法中不再直接执行具体的回调,而是先定义一个参数handlers将onFulfilled, onRejected,resolve与reject四个回调函数保存起来,等得知具体执行结果后再根据情况进行调用,如图中所示,值得注意的是,因为then方法可以进行多次调用,因此将handlers属性设置为一个数组会更为合适。此外,再额外定义一个run方法专门用于执行then方法中传递的回调,而在改变状态的changeState方法与then方法中调用这个run方法即可。
根据思路可以将代码完善如下
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
#state = PENDING
#result = undefined
#handlers = [] // 定义一个参数handler数组,用来存储then方法中传入的回调函数
// 定义一个run方法,专门用于根据不同情况,执行then方法中的不同的回调函数
#run() {
// 占位
}
constructor(executor) {
const resolve = (data) => {
this.#changeState(FULFILLED, data)
}
const reject = (reason) => {
this.#changeState(REJECTED, reason)
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
#changeState(state, result) {
if (this.#state !== PENDING) return
this.#state = state
this.#result = result
this.#run() // 执行定义好的run方法,以确保能执行then方法中异步传入结果的回调函数也能正常执行
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#handlers.push({ // 向handlers数组中添加then方法中传入的每一组回调函数
onFulfilled, onRejected, resolve, reject
})
this.#run() // 直接执行
})
}
}
实现了上述逻辑后,就该具体分析一下run方法内应该如何执行了,首先判断此时Promise的状态,如果还是pending,说明还没进行过状态变更,后续代码不用执行,直接返回即可;如果状态不再是pending,此时,对handlers进行循环遍历,依次取出handlers数组中的每一项,再根据当前Promise的状态来决定是执行onFulfilled还是onRejected,同时将Promise的结果作为函数的参数传入。
class MyPromise {
...
// 定义一个run方法,专门用于根据不同情况,执行then方法中的不同的回调函数
#run() {
if (this.#state === PENDING) return
while (this.#handler.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
if (this.#state === FULFILLED) {
onFulfilled(this.#result)
} else if (this.#state === REJECTED) {
onRejected(this.#result)
}
}
}
...
}
好了,先执行一下,看看结果如何,首先是成功的回调
再看一下失败的情况
成功和失败的回调均能正常输出,没有问题,接下来改判断then方法中返回的Promise的状态了。(解决上图中MyPromise状态一直是pending)
代码写到这里。其实出现了三种情况,分别是:
- 外界调用then方法时,传递的回调onFulfilled和onRejected不是函数
- 外界调用then方法时,传递的回调onFulfilled和onRejected不是函数
逐个情况分析一下,首先第一种情况,当外部调用时传递的不是一个函数,比如是个字符串或者null,或者干脆什么也不传,根据ES6实现的Promise来看,此时返回的Promise的状态应该和调用then方法的Promise状态和结果保持一致,(也被称为Promise的穿透性)根据这种情况,我们可以在实现时判断,如果onFulfilled或onRejected不是一个函数时,判断此时MyPromise的状态,如果是成功态,则直接执行resolve,将此时的结果作为参数传入;如果是失败态,也直接调用reject函数,将结果作为原因传入,实现代码如下。
class MyPromise {
...
// 定义一个run方法,专门用于根据不同情况,执行then方法中的不同的回调函数
#run() {
if (this.#state === PENDING) return
while (this.#handler.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
if (this.#state === FULFILLED) {
if(typeof onFulfilled === 'function'){
onFulfilled(this.#result)
}else{
resolve(this.#result)
}
} else if (this.#state === REJECTED) {
if(typeof onFulfilled === 'function'){
onRejected(this.#result)
}else{
reject(this.#result)
}
}
}
}
接下来就是第二种情况,即onFulfilled或onRejected是一个函数时,此时就需要运行这个函数,需要注意的是,此时应该进行判断,看onFulfilled或onRejected在运行过程中是否有报错,如果有报错,则应该执行reject,反正,正常运行则应该执行resolve。因此需要用try…catch包裹住执行函数,在try中正常执行函数,如果没有报错则把函数的返回结果保存下来,执行resolve并把这个返回结果作为resolve的参数传如;在catch中捕获报错,执行reject,并把错误原因作为参数传入reject。
// 定义一个run方法,专门用于根据不同情况,执行then方法中的不同的回调函数
#run() {
if (this.#state === PENDING) return
while (this.#handler.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
if (this.#state === FULFILLED) {
if (typeof onFulfilled === 'function') {
try {
const data = onFulfilled(this.#result) // 判断回调函数执行是否报错
resolve(data)
} catch (error) {
reject(error)
}
} else {
resolve(this.#result)
}
} else if (this.#state === REJECTED) {
if (typeof onFulfilled === 'function') {
try {
const data = onRejected(this.#result)
resolve(data)
} catch (error) {
reject(error)
}
} else {
reject(this.#result)
}
}
}
}
接下来就是最后一种情况了,也就是执行了onFulfilled或onRejected后返回的还是一个Promise,此时具体执行的是resolve还是reject就得看返回的Promise的结果到底是成功还是失败了。因此首先需要检测执行函数返回结果是不是一个Promise,这里定义一个isPromiseLike的辅助函数专门判断处理,当判断返回结果是一个Promise时,调用返回结果的then方法,把resolve和reject方法作为返回结果的then方法的两个回调传进去,如果返回结果的Promise成功,就会调用resolve,失败就会调用reject。
// 判断是否是符合Promise A+规范的promise
#isPromiseLike(value) {
return false // 先作为占位,之后再实现
}
#run() {
if (this.#state === PENDING) return
while (this.#handler.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
if (this.#state === FULFILLED) {
if (typeof onFulfilled === 'function') {
try {
const data = onFulfilled(this.#result)
if (this.#isPromiseLike(data)) { // 判断返回结果是否是一个Promise
data.then(resolve,reject) // 是就执行返回结果的then方法
} else {
resolve(data)
}
} catch (error) {
reject(error)
}
} else {
resolve(this.#result)
}
} else if (this.#state === REJECTED) {
if (typeof onFulfilled === 'function') {
try {
const data = onRejected(this.#result)
if (this.#isPromiseLike(data)) {
data.then(resolve, reject)
} else {
resolve(data)
}
} catch (error) {
reject(error)
}
} else {
reject(this.#result)
}
}
}
}
写到这里,我们会发现run方法中if与else if内的两中情况逻辑高度相似,完全可以抽离成一个单独的方法runOne,抽离后的代码如下。
#runOne(callback, resolve, reject) { // callback 回调函数(onFulfilled||onRejected)
if (typeof callback !== 'function') { // 判断传入的回调函数是否是函数,不是则走穿透逻辑
const settled = this.#state === FULFILLED ? resolve : reject // 定义一个变量settled,根据当前的state判断此次应该执行什么函数
settled(this.#result)
return; // 确定不是函数,后面的逻辑也不会再走了
}
try {
const data = callback(this.#result)
if (this.#isPromiseLike(data)) { // 回调函数正常执行且回调函数的返回结果是个Promise,需要调用返回结果的then方法,并把resolve和reject传进去,由返回结果的Promise判断应该执行哪个
data.then(resolve, reject)
} else {
resolve(data) // 回调函数正常执行且回调函数的返回结果不是个Promise,执行resolve
}
} catch (error) {
reject(error) // 捕获到了回调函数执行时抛出的错误,此时无论state是什么都会执行reject
}
}
#run() {
if (this.#state === PENDING) return
while (this.#handler.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
if (this.#state === FULFILLED) {
this.#runOne(onFulfilled, resolve, reject) // 成功传递onFulfilled
} else if (this.#state === REJECTED) {
this.#runOne(onRejected, resolve, reject)// 失败传递onRejected
}
}
}
逻辑抽离完成了,接下来改完善之前定义的用于判断是否是Promise的辅助函数。
根据Promise A+规范,不管是对象还是函数,只要它包含了一个叫做then的函数的属性,都可以叫Promise,因此判断方法也很简单
#isPromiseLike(value){
if (value && (typeof value === 'object' || typeof value === 'function')) {
return typeof value.then==='function'
}
return false
}
PS:为什么这里需要这个辅助函数而不是直接采用 data instanceof MyPromise进行判断呢?
答案:因为判断一个东西是否是Promise,应该是看它是不是满足Promise A+规范,Promise之间应该具有互操作性,比如ES6的Promise和我们创建的MyPromise直接就应该可以互相操作,但如果直接用 data instanceof MyPromise判断一个由ES6创建的Promise时,这个结果肯定是false,反之,如果用 data instanceof MyPromise判断一个由MyPromise创建的Promise时,结果也肯定是false,但实际上这两种情况都应该返回true才对,因此用instanceof判断无法满足这里的需要。
功能大致实现了,但还有一点需要注意,运行then方法中的回调应该是放在微队列里的,因此还需要一个辅助函数实现将方法放入微队列的功能,这里定义一个runMicroTask,具体实现逻辑参考vue源码,分成三种情况
- node环境:用process.nextTick模拟微队列
- 浏览器:用MutationObserver模拟微队列
- 既不在浏览器也不在node环境,此时没办法了,因为微队列是环境提供的能力,脱离环境JS是无法实现的,因此只能用setTimeout作为替代
// 将任务放入异步队列里
#runMicroTask(func) {
if (typeof process === 'object' && typeof process.nextTick === 'function') { // 模拟node环境微队列执行
process.nextTick(func)
} else if (typeof MutationObserver === 'function') { // 模拟浏览器环境的微队列执行
const ob = new MutationObserver(func) // MutationObserver是一个观察器,它会自动将一个回调函数放入微队列
const textNode = document.createTextNode('1') // 构建一个文本节点用于观察器观察
ob.observe(textNode, { // 指定观察器观察的节点
characterData: true // 观察字符的变化
})
textNode.data='2' // 手动修改文本节点,当被观察的对象发生变化时,观察器会把回调放入微队列执行,从而实现模拟微队列
} else {
setTimeout(func, 0) // 既不是node环境也不是浏览器,只能用setTimeout来实现异步了
}
}
...
#runOne(callback, resolve, reject) {
this.#runMicroTask(() => { // 将之前的逻辑通过一个箭头函数的形式放入runMicroTask实现模拟放入微队列
if (typeof callback !== 'function') {
const settled = this.#state === FULFILLED ? resolve : reject
settled(this.#result)
return;
}
try {
const data = callback(this.#result)
if (this.#isPromiseLike(data)) {
data.then(resolve, reject)
} else {
resolve(data)
}
} catch (error) {
reject(error)
}
})
}
...
ok,全部写完,找几个结果测试一下
成功在1秒后输出,状态也发生了改变
输出顺序也没有问题。
至此,基于Promise A+规范的MyPromise代码全部完成。Promise的其余方法,如catch,finally,resolve,reject,all等方法,这些都是ES6中额外加入的,Promise A+规范中并未对这些方法的实现有提及,因此这些方法的实现放在下一篇文章中。
总结
本篇文章,着重对MyPromise中的then方法的执行时机和返回结果进行了实现,并完成了检测是否是Promise和实现微队列效果的两个辅助函数。
最后,附上MyPromise的完整代码
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
#state = PENDING
#result = undefined
#handler = []
// 判断是否是符合Promise A+规范的promise
#isPromiseLike(value) {
if (value && (typeof value === 'object' || typeof value === 'function')) {
return typeof value.then === 'function'
}
return false
}
// 将任务放入异步队列里
#runMicroTask(func) {
if (typeof process === 'object' && typeof process.nextTick === 'function') { // 模拟node环境微队列执行
process.nextTick(func)
} else if (typeof MutationObserver === 'function') { // 模拟浏览器环境的微队列执行
const ob = new MutationObserver(func) // MutationObserver是一个观察器,它会自动将一个回调函数放入微队列
const textNode = document.createTextNode('1') // 构建一个文本节点用于观察器观察
ob.observe(textNode, { // 指定观察器观察的节点
characterData: true // 观察字符的变化
})
textNode.data='2' // 手动修改文本节点,当被观察的对象发生变化时,观察器会把回调放入微队列执行,从而实现模拟微队列
} else {
setTimeout(func, 0) // 既不是node环境也不是浏览器,只能用setTimeout来实现异步了
}
}
#runOne(callback, resolve, reject) { // callback 回调函数(onFulfilled||onRejected)
this.#runMicroTask(() => { // 将之前的逻辑通过一个箭头函数的形式放入runMicroTask实现模拟放入微队列
if (typeof callback !== 'function') { // 判断传入的回调函数是否是函数,不是则走穿透逻辑
const settled = this.#state === FULFILLED ? resolve : reject // 定义一个变量settled,根据当前的state判断此次应该执行什么函数
settled(this.#result)
return; // 确定不是函数,后面的逻辑也不会再走了
}
try {
const data = callback(this.#result)
if (this.#isPromiseLike(data)) { // 回调函数正常执行且回调函数的返回结果是个Promise,需要调用返回结果的then方法,并把resolve和reject传进去,由返回结果的Promise判断应该执行哪个
data.then(resolve, reject)
} else {
resolve(data) // 回调函数正常执行且回调函数的返回结果不是个Promise,执行resolve
}
} catch (error) {
reject(error) // 捕获到了回调函数执行时抛出的错误,此时无论state是什么都会执行reject
}
})
}
#run() {
if (this.#state === PENDING) return
while (this.#handler.length) {
const { onFulfilled, onRejected, resolve, reject } = this.#handler.shift()
if (this.#state === FULFILLED) {
this.#runOne(onFulfilled, resolve, reject) // 成功传递onFulfilled
} else if (this.#state === REJECTED) {
this.#runOne(onRejected, resolve, reject)// 失败传递onRejected
}
}
}
constructor(executor) {
const resolve = (data) => {
this.#changeState(FULFILLED, data)
}
const reject = (reason) => {
this.#changeState(REJECTED, reason)
}
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
#changeState(state, result) {
if (this.#state !== PENDING) return
this.#state = state
this.#result = result
this.#run()
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this.#handler.push({
onFulfilled, onRejected, resolve, reject
})
this.#run()
})
}
}