本文已参与「新人创作礼」活动,一起开启掘金创作之路
本文根据Promise A+
规范,带你一步一步由浅入深实现Promise
,耐心看完绝对能够理解并实现!
源码在我的github中,有需要可以自取:github.com/Plasticine-…
1. 基本功能实现
首先要明确Promise
是一个构造函数,能够接收一个executor
参数,该参数是一个函数,它接收两个参数executor(resolve, reject)
那么在我们的MyPromise
的构造函数中是不是就应该要去执行这个executor
函数,并且传入resolve
和reject
参数,这两个参数是函数,并且在我们的MyPromise
中去实现
由于每一个Promise
实例的resolve
和reject
函数都应当是相互独立的,也就是都是属于实例自身的,因此不适合定义在构造函数的prototype
上,在ES6 class
的角度来看,就是说不应当将resolve
和reject
函数定义为类的方法,而应当在构造函数内部定义,这样就能保证它们是会在每个实例实例化调用构造函数的时候在内存中创建多个,如果定义为方法的话,就会挂到构造函数的prototype
上
根据以上的特点,我们可以先写一个整体的框架
class MyPromise {
constructor(executor) {
const resolve = () => {}
const reject = () => {}
executor(resolve, reject)
}
}
我们都知道,Promise
是有状态的,根据Promises A+
规范,有三种状态
A promise must be in one of three states: pending, fulfilled, or rejected.
那么现在我们就给我们的MyPromise
定义三个状态常量
/**
* @description MyPromise 的状态
*/
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
每个Promise
实例在初始的时候都是pending
状态,除非外部调用了resolve
或者reject
函数才会改变promise实例的状态,因此构造函数中应当先初始化状态为pending
constructor(executor) {
this.status = PENDING
}
其次,根据Promises A+
规范,每个Promise
实例都能够调用then
方法,由onFulfilled
回调去处理resolved
状态的value
,以及由onRejected
回调去处理rejected
状态的reason
,那么我们还需要给实例维护一个value
和reason
以便之后能够被相应的回调使用
constructor(executor) {
this.value = undefined
this.reason = undefined
}
这里初始化为undefined
是因为外部如果调用resolve
或者reject
函数时没有传入参数的话,在js中未传入实参时去访问形参就是undefined
,因此onFulfilled/onRejected
回调中的value/reason
也应当是undefined
,这是为了和js的函数特性统一
function foo(text) {
console.log(text)
}
foo() // => undefined
接下来就是在resolve/reject
中处理状态的变更以及设置相应的value/reason
了
constructor(executor) {
const resolve = (value) => {
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
}
}
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
}
}
executor(resolve, reject)
}
这里有几点值得注意:
- 状态的变更只能是从
pending
变到fulfilled/rejected
,而不能是其他的变化,因此在变更状态之前一定要判断一下当前状态是不是pending
value/reason
的修改一定是在状态变更后才生效的,因此也要放到判断语句中- 最重要的一点!!!
resolve/reject
函数只能用箭头函数去定义,一定不能用普通函数去定义,这涉及到箭头函数和普通函数中this
指向的问题- 因为需要在
resolve/reject
中使用到this
,并且一定要保证this
是指向MyPromise
实例的 - 普通函数的 this 指向是在外部调用的时候决定的,会因为
this
的默认绑定、隐式绑定、显式绑定等特性导致this
的指向不明确,不能保证指向实例本身 - 箭头函数中没有
this
,根据js
词法作用域的特点,会使用函数定义时的父函数定义域中的this
,也就是constructor
函数中的this
- 因为需要在
- 而箭头函数要用 const 关键字声明一个引用变量去指向它,因此要在执行 executor 之前定义,否则会由于暂时性死区导致无法找到 resolve, reject 函数
接下来就是then
方法的实现了,then
方法接收两个参数,onFulfilled
和onRejected
,是两个回调函数,根据Promises A+
规范,调用then
方法会在fulfilled
状态时执行onFullfilled
回调,在rejected
状态时执行onRejected
回调,根据这一特性,可以写出如下代码:
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
}
现在就已经得到一个可以实现基本功能的Promise
了,目前的完整代码如下:
/**
* @description MyPromise 的状态
*/
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor(executor) {
this.status = PENDING // 初始状态为 PENDING
this.value = undefined // resolve 出去的值
this.reason = undefined // reject 出去的原因
const resolve = (value) => {
// 只有在 PENDING 状态时才能转换到 FULFILLED 状态
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
}
}
const reject = (reason) => {
// 只有在 PENDING 状态时才能转换到 REJECTED 状态
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
}
}
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
}
}
测试一下
function foo() {
return new MyPromise((resolve, reject) => {
resolve('resolved value')
})
}
foo().then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
}
)
// => resolved value
function foo() {
return new MyPromise((resolve, reject) => {
reject('rejected reason')
})
}
foo().then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
}
)
// => rejected reason
2. 异常处理
目前我们的MyPromise
在遇到异常的时候会怎样呢?能够像Promise
那样被onRejected
捕获到吗?
很明显是不行的,因为我们并没有对异常进行任何的处理。那么思考一下异常的处理应该加在什么地方才能让异常被捕获到并且传给onRejected
回调去处理呢?
首先要明确异常从哪里来,异常肯定是从外部调用构造函数时的executor
函数中来的,因为主要的业务逻辑都是在executor
中编写的
因此如果在调用executor
的过程中出现了异常,我们应当在我们的MyPromise
中去捕获它,并且因为希望被onRejected
处理,因此需要将状态改为rejected
,并将reason
设置为捕获到的异常,这部分逻辑不就是reject
函数做的事情吗?因此我们可以直接复用,调用reject
即可
constructor(executor) {
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
现在异常就能够被onRejected
处理了
function foo() {
return new MyPromise((resolve, reject) => {
throw new Error('something wrong...')
})
}
foo().then(
(value) => {
console.log(`resolved: ${value}`)
},
(reason) => {
console.log(`rejected: ${reason}`)
}
)
// => rejected: Error: something wrong...
3. 使用发布-订阅模式解决异步和多次调用的问题
目前我们的MyPromise
中如果遇到了异步执行的情况会怎么样?比如说在executor
中,延时两秒才去执行resolve
函数,那么能被then
方法的onFulfilled
处理吗?
function foo() {
return new MyPromise((resolve, reject) => {
setTimeout(() => resolve('resolved value'), 2000)
})
}
foo().then(
(value) => {
console.log(`resolved: ${value}`)
},
(reason) => {
console.log(`rejected: ${reason}`)
}
)
// =>
可以看到什么都没有输出,因为executor
的执行是同步的,遇到setTimeout
的时候会将执行回调放到事件循环的宏任务队列中,接着立刻去执行then
方法了, 而此时由于还没有调用resolve
,因此状态还是pending
,那么then
自然也就无法处理了
等到两秒后定时器中的回调执行了,改变了状态,但此时js线程中的代码早已经执行完了,也就是then
早就结束了,因此没有别的代码能够处理resolve
后的resolved
状态
或许我们会想出下面这样的解决方案
function foo() {
return new MyPromise((resolve, reject) => {
// resolve('resolved value')
// reject('rejected reason')
// throw new Error('something wrong...')
setTimeout(() => resolve('resolved value'), 2000)
})
}
const myPromise = foo()
// 2.5 秒后再去执行 then 方法处理 resolved 状态
setTimeout(() => {
myPromise.then(
(value) => {
console.log(`resolved: ${value}`)
},
(reason) => {
console.log(`rejected: ${reason}`)
}
)
}, 2500)
// => resolved: resolved value
注意:两个定时器几乎是同时开始计时的,因此最终打印出**resolved: resolved value**
所花费的时间不是**2 + 2.5 = 4.5秒**
,而是**2.5秒**
这种方案貌似确实能够解决异步调用的问题,但它真的解决了吗?
试想一下,如果我们现在不是用定时器,而是一个axios/fetch
的ajax
请求呢?我们只会在请求有结果之后才去调用resolve
函数,还能这样定死一个2.5秒的定时器去调用then
方法吗?
显然是不行的,因为并不知道这个ajax请求具体会花费多少时间,如果运气好,它在2.5秒内返回结果了,那么then
还是能够正常处理结果的,但是如果超出了2.5秒就不行了,难道要设置一个超级长时间的定时器,到期后再去执行then
方法吗?显然太离谱了
这就要考虑再改进一下我们的MyPromise
了,我们需要用到**发布-订阅**
这一设计模式来解决这一问题
首先思考一下,之所以会出现异步调用无法触发onResolved
回调,不正是因为在同步代码中执行then
方法的时候,异步代码还没调用resolve
呢,因此整个MyPromise
实例的状态还是pending
状态
既然如此,那么我们可以在then
方法中处理pending
状态呀,维护一个容器,专门用于存放onResolved
回调(因为可能会调用多次then
方法,每个then
方法都有自己的onResolved
回调),然后在异步代码调用resolve
的时候,从这个容器中依次取出onResolved
回调并且执行不就可以了吗!
事实上:
then
中处理pending
,将onResolved
和onRejected
分别加入到对应容器中这一过程就是“订阅”的过程,订阅fulfilled
和rejected
事件resolve
将容器中的onResolved
回调依次取出执行的过程就是“发布”的过程,发布fulfilled
消息reject
也是类似的,只是它是从存放onRejected
回调的容器中去取回调而已,也是一个“发布”的过程,发布的是rejected
消息
直接看代码吧!
then(onFulfilled, onRejected) {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
if (this.status === PENDING) {
// pending 状态下需要 “订阅” fulfilled 和 rejected 事件
this.onFulfilledCallbackList.push(() => onFulfilled(this.value))
this.onRejectedCallbackList.push(() => onRejected(this.reason))
}
}
const resolve = (value) => {
// 只有在 PENDING 状态时才能转换到 FULFILLED 状态
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
// 触发 resolve 行为,开始 “发布” resolved 事件
this.onFulfilledCallbackList.forEach((fn) => fn())
}
}
const reject = (reason) => {
// 只有在 PENDING 状态时才能转换到 REJECTED 状态
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
// 触发 reject 行为,开始 “发布” rejected 事件
this.onRejectedCallbackList.forEach((fn) => fn())
}
}
现在就可以正常处理异步代码了
function foo() {
return new MyPromise((resolve, reject) => {
setTimeout(() => resolve('resolved value'), 2000)
})
}
foo().then(
(value) => {
console.log(`resolved: ${value}`)
},
(reason) => {
console.log(`rejected: ${reason}`)
}
)
// => resolved: resolved value
并且这样设计还有一个好处,对于多次调用了then
方法的时候,我们肯定是希望按照调用then
方法的顺序去执行它们当中的回调的,而由于我们维护的两个容器实际上就是一个队列,回调都是先进先出的,这样就保证了回调执行的顺序和then
方法的调用顺序是一致的了
function foo() {
return new MyPromise((resolve, reject) => {
setTimeout(() => resolve('resolved value'), 2000)
})
}
const myPromise = foo()
myPromise.then(
(value) => {
console.log(`resolved1: ${value}`)
},
(reason) => {
console.log(`rejected1: ${reason}`)
}
)
myPromise.then(
(value) => {
console.log(`resolved2: ${value}`)
},
(reason) => {
console.log(`rejected2: ${reason}`)
}
)
myPromise.then(
(value) => {
console.log(`resolved3: ${value}`)
},
(reason) => {
console.log(`rejected3: ${reason}`)
}
)
/**
=> resolved1: resolved value
resolved2: resolved value
resolved3: resolved value
*/
4. 解决链式调用的问题
原生的Promise
是支持链式调用的
const promise = new Promise((resolve, reject) => {
resolve('plasticine')
})
promise
.then((value) => {
console.log(`then1 -- ${value}`)
return value
})
.then((value) => {
console.log(`then2 -- ${value}`)
return new Promise((resolve, reject) => resolve(value))
})
.then((value) => {
console.log(`then3 -- ${value}`)
return Promise.resolve(value)
})
.then((value) => {
console.log(`then4 -- ${value}`)
})
/**
=> then1 -- plasticine
then2 -- plasticine
then3 -- plasticine
then4 -- plasticine
*/
目前我们的MyPromise
还不支持这样的功能,为什么能够链式调用呢?那肯定是因为then
方法返回的是Promise
实例呗,事实上Promises A+
规范中明确规定了:
then must return a promise.
promise2 = promise1.then(onFulfilled, onRejected);
- If either
onFulfilled
oronRejected
returns a valuex
, run the Promise Resolution Procedure[[Resolve]](promise2, x)
.- If either
onFulfilled
oronRejected
throws an exceptione
, promise2 must berejected
with e as thereason
.- If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
- If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.
4.1 then中返回Promise实现链式调用
那么根据这个规范,我们可以修改一下then
方法的实现,包裹一层MyPromise
对象promise2
,并且将这个对象返回
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
onFulfilled(this.value)
}
if (this.status === REJECTED) {
onRejected(this.reason)
}
if (this.status === PENDING) {
// pending 状态下需要 “订阅” fulfilled 和 rejected 事件
this.onFulfilledCallbackList.push(() => onFulfilled(this.value))
this.onRejectedCallbackList.push(() => onRejected(this.reason))
}
})
return promise2
}
4.2 捕获then
中两个回调的返回值x
规范的第一条里还提到了,onFulFilled
和onRejected
回调会返回一个x
,那么就加上呗
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
let x = onFulfilled(this.value)
}
if (this.status === REJECTED) {
let x = onRejected(this.reason)
}
if (this.status === PENDING) {
// pending 状态下需要 “订阅” fulfilled 和 rejected 事件
this.onFulfilledCallbackList.push(() => {
let x = onFulfilled(this.value)
})
this.onRejectedCallbackList.push(() => {
let x = onRejected(this.reason)
})
}
})
return promise2
}
然后这个x
是会被resolve
出去的,这样才能够被下一个then
方法接收到
那么就要考虑一下x
会是什么了,从TS
的角度来说,x
是any
类型的,可能是基本数据类型也可能是引用数据类型,这都无所谓,麻烦的是x
还可能是MyPromise
对象,那么这就需要好好研究一下了
因为如果直接把MyPromise
对象给resolve
出去,下一个then
拿到的是一个MyPromise
对象,它没办法拿到真正想要的value
,因此对于x
是MyPromise
对象的情况,应当执行它里面的resolve
函数
4.3 处理then回调执行过程中抛出的异常
规范的第二条中提到了对于onFulfilled
和onRejected
中的异常,会作为promise2
的reason
被reject
出去,那我们就try/catch
捕获一下,遇到异常就调用promise2
的reject
即可
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
try {
let x = onFulfilled(this.value)
} catch (e) {
reject(e)
}
}
if (this.status === REJECTED) {
try {
let x = onRejected(this.reason)
} catch (e) {
reject(e)
}
}
if (this.status === PENDING) {
// pending 状态下需要 “订阅” fulfilled 和 rejected 事件
this.onFulfilledCallbackList.push(() => {
try {
let x = onFulfilled(this.value)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbackList.push(() => {
try {
let x = onRejected(this.reason)
} catch (e) {
reject(e)
}
})
}
})
return promise2
}
4.4 resolvePromise处理x
接下来我们就应该去处理一下x
了,规范第一条中说到[[Resolve]](promise2, x)
其实就是要调用[[Resolve]]
函数去处理promise2
实例以及返回的x
同样的,规范中有说明,这个[[Resolve]]
函数在规范中的命名为resolvePromise
,那我们就应当在拿到promise2
和x
之后去调用这样一个函数
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x)
} catch (e) {
reject(e)
}
}
...
}
4.4.1 异步执行resolvePromise保证参数能够获取到
但是有一个问题,promise2
能够拿得到吗?promise2
是在MyPromise
的构造函数执行完毕后才会得到的,而现在我们却在构造函数内部提前要用到promise2
实例,这显然有点问题
其次,我们之后是需要调用promise2
的resolve/reject
来处理x
的,但是它们是定义在MyPromise
构造函数内部的,因此resolvePromise
中无法访问到,因此我们还需要将resolve/reject
也传给resolvePromise
函数
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}
...
}
好了,那么接下来就要解决如何获取到promise2
的问题了,这个问题在Promises A+
规范中也有提到
onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
可以看到,可以使用宏任务或者微任务的方式去实现,这样一来,整个构造函数的执行会在js线程执行的时候去执行,宏任务会被添加到宏任务队列中,然后到了执行宏任务的时候,promise2
实例已经是创建好了的,因此resolvePromise
就可以正常使用它了
当然,也可以用微任务的方式,原生的Promise
就是以微任务的方式实现的,这里为了简单起见,直接用setTimeout
实现,它是宏任务的方式实现的
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
// 以宏任务的方式执行 resolvePromise 才能保证拿到 promise2 实例
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x)
} catch (e) {
reject(e)
}
}, 0)
}
})
}
这里**setTimeout**
的延时默认就是0,不填也可以,我填了只是为了强调一下这里使用**setTimeout**
仅仅是为了以宏任务的方式去执行**resolvePromise**
,让他在事件循环的下一轮执行,而不是立即执行,且要注意的是,虽然是**0ms**
的延迟,但实际上还是会有至少**4ms**
的延迟,这一点**MDN**
上有解释,自行查阅即可
4.4.2 实现resolvePromise
接下来就要实现一下resolvePromise
了,resolvePromise
的实现也需要按照Promises A+
的规范去实现
4.4.2.1 循环引用时抛出异常
首先根据2.3.1
,如果promise
和x
引用的是同一个对象,则应该reject
一个TypeError
作为reason
先用原生Promise
示范一下,加深理解
const promise1 = new Promise((resolve, reject) => {
resolve('plasticine')
})
const promise2 = promise1.then(
(value) => {
return promise2
},
(reason) => {
return reason
}
)
promise2.then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
}
)
// => TypeError: Chaining cycle detected for promise #<Promise>
那么我们可以在resolvePromise
中判断一下,promise === x
时reject
一个TypeError
出去
/**
*
* @param {MyPromise} promise MyPromise 实例
* @param {any} x 前一个 MyPromise resolve 出来的值
* @param {Function} resolve promise 对象构造函数中的 resolve
* @param {Function} reject promise 对象构造函数中的 reject
*/
function resolvePromise(promise, x, resolve, reject) {
if (promise === x) {
return reject(
new TypeError('Chaining cycle detected for promise #<MyPromise>')
)
}
}
4.4.2.2 判断x是否是MyPromise实例
接下来看一下2.3.3
(2.3.2
可以跳过,实际上就是告诉我们要保持promise
的状态而已)
2.3.3
需要判断一下x
是否是一个object/function
然后2.3.3.1
又要令then = x.then
,然后会判断then
是否是一个函数
这样做的目的就是用于判断x
是否是一个MyPromise
对象,有then
方法则将其视为是MyPromise
对象
取**then = x.then**
这一过程需要注意到一点,**x.then**
可能被**Object.defineProperty**
设置了**getter**
劫持,如果**getter**
中抛出了异常,需要将这个异常作为**reason**
把它**reject**
出去,这也正是**2.3.3.2**
中规定的
Object.defineProperty(x, 'then', {
get() {
throw new Error('something wrong...')
}
})
因此我们需要用try/catch
包裹一下let then = x.then
,然后根据2.3.3.3.1
和2.3.3.3.2
,当x
是一个MyPromise
对象的时候,我们需要调用它的then
方法,并且需要显式绑定this
为x
,同时传入两个回调:resolvePromise
和rejectPromise
此**resolvePromise**
不是外面的整个**resolvePromise(promise, x, resolve, reject)**
,而是另外定义的,这里我们直接用箭头函数的形式传入,是两个匿名函数
且根据2.3.3.3.1
,resolvePromise
回调接收一个参数y
,然后会递归调用resolvePromise(promise, y, resolve, reject)
,因为如果x
是一个MyPromise
的话,它的then
当中仍然可能返回MyPromise
对象,因此需要递归处理
根据以上几点,我们现在得到的resolvePromise(promise, x, resolve, reject)
如下:
/**
* 根据 x 的类型作出不同的处理
* x 为非 MyPromise 对象的时候 -- 直接 resolve
* x 为 MyPromise 对象的时候 -- 调用其 resolve
*
* 剩下的还有一些细节会具体在代码中注释
*
* @param {MyPromise} promise MyPromise 实例
* @param {any} x 前一个 MyPromise resolve 出来的值
* @param {Function} resolve promise 对象构造函数中的 resolve
* @param {Function} reject promise 对象构造函数中的 reject
*/
function resolvePromise(promise, x, resolve, reject) {
// 根据 Promises A+ 规范 2.3.1: promise 和 x 指向同一个引用时应当 reject 一个 TypeError
if (promise === x) {
return reject(
new TypeError('Chaining cycle detected for promise #<MyPromise>')
)
}
// 2.3.3: x 是 object or function 时的情况
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// typeof null === 'object',因此需要额外判断一下排除 x 是 null 的情况
try {
// x.then 可能被 Object.defineProperty 设置了 getter 劫持,并抛出异常,因此要 try/catch 捕获
let then = x.then
if (typeof then === 'function') {
// x 有 then 方法时,就将其视为是 MyPromise 对象
// 2.3.3.3 执行 x.then,并且要显式绑定 this 指向,且有两个回调 resolvePromise 和 rejectPromise
then.call(
x,
// resolvePromise
(y) => {
// 需要递归调用 MyPromise 中 resolve 出去的值,也就是这里的 y
resolvePromise(promise, y, resolve, reject)
},
// rejectPromise
(r) => {
// 对于 reject,直接将其 reason 给 reject 出去即可
reject(r)
}
)
} else {
// x 没有 then 方法 -- 不需要特殊处理,直接 resolve
resolve(x)
}
} catch (e) {
reject(e)
}
} else {
// 基本数据类型 -- 直接 resolve
resolve(x)
}
}
4.4.2.3 防止多次执行then中的回调
根据2.3.3.3.3
,当两个回调都被执行的时候,我们应当只执行第一个,什么意思呢?用原生Promise
演示一下
const promise1 = new Promise((resolve, reject) => {
resolve('plasticine')
})
const promise2 = promise1.then(
(value) => {
return new Promise((resolve, reject) => {
resolve(value)
reject(new Error('something wrong...'))
})
},
(reason) => {
return reason
}
)
promise2.then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
}
)
// => plasticine
也就是如果在then
的onFulfilled
回调中,返回新的Promise
实例时,在构造函数中既调用了resolve
又调用了reject
的话,只会执行第一个被调用的,剩下的会被忽略,这个例子中reject
就被忽略了
那么我们的MyPromise
要如何实现这个功能呢?可以用一个布尔值进行标记,表示x.then
中的两个回调是否有任何一个被调用过,有的话则不会再去执行它们
function resolvePromise(promise, x, resolve, reject) {
// 根据 Promises A+ 规范 2.3.1: promise 和 x 指向同一个引用时应当 reject 一个 TypeError
if (promise === x) {
return reject(
new TypeError('Chaining cycle detected for promise #<MyPromise>')
)
}
// 2.3.3: x 是 object or function 时的情况
let isCalled = false // 当 x 是 MyPromise 实例时,标记 x.then 中的回调是否有任何一个被调用过
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// typeof null === 'object',因此需要额外判断一下排除 x 是 null 的情况
try {
// x.then 可能被 Object.defineProperty 设置了 getter 劫持,并抛出异常,因此要 try/catch 捕获
let then = x.then
if (typeof then === 'function') {
// x 有 then 方法时,就将其视为是 MyPromise 对象
// 2.3.3.3 执行 x.then,并且要显式绑定 this 指向,且有两个回调 resolvePromise 和 rejectPromise
then.call(
x,
// resolvePromise
(y) => {
// 需要递归调用 MyPromise 中 resolve 出去的值,也就是这里的 y
if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行
isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 rejectPromise
resolvePromise(promise, y, resolve, reject)
},
// rejectPromise
(r) => {
// 对于 reject,直接将其 reason 给 reject 出去即可
if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行
isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 resolvePromise
reject(r)
}
)
} else {
// x 没有 then 方法 -- 不需要特殊处理,直接 resolve
resolve(x)
}
} catch (e) {
// 如果出错了的话,不应该还能调用 resolve,因此这里也要加上 isCalled 判断,防止去调用 resolve
if (isCalled) return
isCalled = true
reject(e)
}
} else {
// 基本数据类型 -- 直接 resolve
resolve(x)
}
}
4.5 功能测试
后面的规范就没什么了,现在可以来测试一下功能是否正常
const promise1 = new MyPromise((resolve, reject) => {
resolve('plasticine')
})
const promise2 = promise1.then(
(value) => {
return new MyPromise((resolve, reject) => {
resolve(value)
reject(new Error('something wrong...'))
})
},
(reason) => {
return reason
}
)
promise2.then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
}
)
// => plasticine
const promise2 = promise1.then(
(value) => {
return new MyPromise((resolve, reject) => {
// 异步调用 resolve
setTimeout(() => {
resolve(value)
}, 2000)
})
},
(reason) => {
return reason
}
)
promise2.then(
(value) => {
console.log(value)
},
(reason) => {
console.log(reason)
}
)
// => plasticine
可以看到,和原生Promise
的效果一致,在onFulfilled
中返回了一个MyPromise
的时候仍然能够在后面的then
中拿到resolve
出去的value
并且既调用了resolve
又调用了reject
,reject
被忽略了,也和原生Promise
表现一致,同时异步调用resolve
也是能正常工作的
5. then回调是可选参数
Promises A+
规范中定义了then
的两个回调onFulfilled
和onRejected
是可选的,也就意味着可以直接.then()
调用
那么这样调用的时候我们应当将resolve
的value
穿透传递下去,直到有then
的回调onFulfilled
去处理,因此我们可以给onFulfilled
和onRejected
默认值
then(onFulfilled, onRejected) {
// onFulfilled 和 onRejected 是可选参数,如果没传的时候应当设置默认值
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
// ...
}
测试一下then
不传入回调的时候是否能够穿透
new MyPromise((resolve, reject) => {
resolve('plasticine')
})
.then()
.then()
.then()
.then()
.then((value) => {
console.log(value)
})
// => plasticine
确实可以!
6. 实现catch
真的实现完了吗?原生Promise
对象还有一个catch
方法呢!
事实上,catch
做的事情就是then
的第二个回调做的事情,都是用于处理reject
出来的reason
的,Promises A+
中并没有规定catch
这个方法,其实可以把catch
方法看成是then
的第二个回调的语法糖,也就是只传入了第二个回调:this.then(null, () => {})
那么我们直接复用then
就可以实现catch
了
catch(onRejected) {
return this.then(null, onRejected)
}
7. 测试是否符合Promises/A+规范
可以使用promises-aplus-tests
这个工具来测试我们的MyPromise
是否符合Promises/A+
规范
首先安装该工具
pnpm i promises-aplus-tests -D
然后在MyPromise.js
的底部添加如下代码,这段代码仅用作该工具测试使用,没有别的意义
// 用于测试 Promises/A+ 规范
MyPromise.defer = MyPromise.deferred = function () {
let deferred = {}
deferred.promise = new MyPromise((resolve, reject) => {
deferred.resolve = resolve
deferred.reject = reject
})
return deferred
}
接下来运行
npx promises-aplus-tests ./MyPromise.js
最后看到全部测试项目均通过!
8. 修复bug -- 在executor中resolve一个MyPromise实例
虽然通过了Promise/A+
规范的测试,但是实际上目前我们的代码是有一个bug
的,看一下如下代码
const promise = new MyPromise((resolve, reject) => {
resolve(
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('resolved value')
}, 2000)
})
)
})
promise.then((res) => {
console.log(res)
})
// =>
// MyPromise {
// status: 'PENDING',
// value: undefined,
// reason: undefined,
// onFulfilledCallbackList: [],
// onRejectedCallbackList: []
// }
当我们resolve
一个MyPromise
实例出去的时候,在这个MyPromise
实例中异步resolve
一个值出去,并不能够被then
捕获到
事实上即便不是异步resolve
,而是直接resolve
出去也是不能被捕获到的,得到的只是一个处于PENDING
状态的MyPromise
实例
那如果我就是希望能够在then
当中得到真正的value
而不是初次被resolve
出去的MyPromise
实例该怎么办呢?因为原生的Promise
就是这样的特性
我们可以对value
是MyPromise
实例的这种情况进行单独处理,调用它里面的then
方法,将真正的value
给resolve
出去
const resolve = (value) => {
// 当 value 是 MyPromise 实例的时候 --> 调用它的 then 方法
if (value instanceof MyPromise) {
value.then(resolve, reject)
// 一定要 return 否则会无限递归下去
return
}
// ...
}
当**value**
是一个**MyPromise**
的时候一定要**return**
退出**resolve**
函数,否则会因为没有**base case**
而无限递归下去
现在这个bug
就修复好啦!
const promise = new MyPromise((resolve, reject) => {
resolve(
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('resolved value')
}, 2000)
})
)
})
promise.then((res) => {
console.log(res)
})
// => resolved value
9. 完整代码
至此,整个Promise
的功能就算是完整实现了,完整代码如下
/**
* @description MyPromise 的状态
*/
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
/**
* 根据 x 的类型作出不同的处理
* x 为非 MyPromise 对象的时候 -- 直接 resolve
* x 为 MyPromise 对象的时候 -- 调用其 resolve
*
* 剩下的还有一些细节会具体在代码中注释
*
* @param {MyPromise} promise MyPromise 实例
* @param {any} x 前一个 MyPromise resolve 出来的值
* @param {Function} resolve promise 对象构造函数中的 resolve
* @param {Function} reject promise 对象构造函数中的 reject
*/
function resolvePromise(promise, x, resolve, reject) {
// 根据 Promises A+ 规范 2.3.1: promise 和 x 指向同一个引用时应当 reject 一个 TypeError
if (promise === x) {
return reject(
new TypeError('Chaining cycle detected for promise #<MyPromise>')
)
}
// 2.3.3: x 是 object or function 时的情况
let isCalled = false // 当 x 是 MyPromise 实例时,标记 x.then 中的回调是否有任何一个被调用过
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// typeof null === 'object',因此需要额外判断一下排除 x 是 null 的情况
try {
// x.then 可能被 Object.defineProperty 设置了 getter 劫持,并抛出异常,因此要 try/catch 捕获
let then = x.then
if (typeof then === 'function') {
// x 有 then 方法时,就将其视为是 MyPromise 对象
// 2.3.3.3 执行 x.then,并且要显式绑定 this 指向,且有两个回调 resolvePromise 和 rejectPromise
then.call(
x,
// resolvePromise
(y) => {
// 需要递归调用 MyPromise 中 resolve 出去的值,也就是这里的 y
if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行
isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 rejectPromise
resolvePromise(promise, y, resolve, reject)
},
// rejectPromise
(r) => {
// 对于 reject,直接将其 reason 给 reject 出去即可
if (isCalled) return // 有任何一个回调已经被执行过了,忽略当前回调的执行
isCalled = true // 首次执行回调,将标志变量置为 true,防止被重复调用或者调用 resolvePromise
reject(r)
}
)
} else {
// x 没有 then 方法 -- 不需要特殊处理,直接 resolve
resolve(x)
}
} catch (e) {
// 如果出错了的话,不应该还能调用 resolve,因此这里也要加上 isCalled 判断,防止去调用 resolve
if (isCalled) return
isCalled = true
reject(e)
}
} else {
// 基本数据类型 -- 直接 resolve
resolve(x)
}
}
class MyPromise {
constructor(executor) {
this.status = PENDING // 初始状态为 PENDING
this.value = undefined // resolve 出去的值
this.reason = undefined // reject 出去的原因
// 维护两个列表容器 存放对应的回调
this.onFulfilledCallbackList = []
this.onRejectedCallbackList = []
/**
* resolve, reject 是两个函数 -- 每个 MyPromise 实例中的 resolve 方法是属于自己的
* 如果将 resolve, reject 定义在构造器外面的话,方法会在构造函数的 prototype 上
* 所以应当在构造器内部定义 resolve, reject 函数
*
* 并且一定要用箭头函数去定义 而不能是普通函数
* 普通函数的 this 指向是在外部调用的时候决定的,不能保证指向实例本身
* 而箭头函数中没有 this,会使用函数定义时的父函数 constructor 中的 this
* 也就是说使用箭头函数能够保证 this 指向实例本身
*
* 而箭头函数要用 const 关键字声明一个引用变量去指向它,因此要在执行 executor 之前定义
* 否则会由于暂时性死区导致无法找到 resolve, reject 函数
*/
const resolve = (value) => {
// 当 value 是 MyPromise 实例的时候
if (value instanceof MyPromise) {
value.then(resolve, reject)
// 一定要 return 否则会无限递归下去
return
}
// 只有在 PENDING 状态时才能转换到 FULFILLED 状态
if (this.status === PENDING) {
this.status = FULFILLED
this.value = value
// 触发 resolve 行为,开始 “发布” resolved 事件
this.onFulfilledCallbackList.forEach((fn) => fn())
}
}
const reject = (reason) => {
// 只有在 PENDING 状态时才能转换到 REJECTED 状态
if (this.status === PENDING) {
this.status = REJECTED
this.reason = reason
// 触发 reject 行为,开始 “发布” rejected 事件
this.onRejectedCallbackList.forEach((fn) => fn())
}
}
try {
executor(resolve, reject)
} catch (e) {
reject(e)
}
}
then(onFulfilled, onRejected) {
// onFulfilled 和 onRejected 是可选参数,如果没传的时候应当设置默认值
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : (value) => value
onRejected =
typeof onRejected === 'function'
? onRejected
: (reason) => {
throw reason
}
const promise2 = new MyPromise((resolve, reject) => {
// 根据状态去判断执行哪一个回调
if (this.status === FULFILLED) {
// 以宏任务的方式执行 resolvePromise 才能保证拿到 promise2 实例
setTimeout(() => {
try {
let x = onFulfilled(this.value)
// 处理 x
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
// 处理 x
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
}
if (this.status === PENDING) {
// pending 状态下需要 “订阅” fulfilled 和 rejected 事件
this.onFulfilledCallbackList.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
// 处理 x
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
this.onRejectedCallbackList.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
// 处理 x
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
catch(onRejected) {
return this.then(null, onRejected)
}
}
// 用于测试 Promises/A+ 规范
MyPromise.defer = MyPromise.deferred = function () {
let deferred = {}
deferred.promise = new MyPromise((resolve, reject) => {
deferred.resolve = resolve
deferred.reject = reject
})
return deferred
}
module.exports = MyPromise