手寫實現滿足 Promise/A+ 規範的 Promise
最近看了 Promise/A+ 的規範,嘗試實現了一個滿足 promises-aplus-tests 測試的 Promise 類,在實現規範的過程中,對於 Promise 本身也加深了理解,這篇文章就將我的實現過程分享出來。
- 本文的程式碼倉庫在這裡,歡迎 Star~。
前置知識
- Promise 是用來解決非同步問題的一個方案,相當於非同步操作的佔位符。
- 每個 Promise 只有三種狀態:
pending
,fulfilled
和rejected
,狀態只能從pending
轉移到fulfilled
或者rejected
,一旦狀態變成fulfilled
或者rejected
2.1.1 When pending, a promise: 2.1.1.1 may transition to either the fulfilled or rejected state. 2.1.2 When fulfilled, a promise: 2.1.2.1 must not transition to any other state. 2.1.2.2 must have a value, which must not change. 2.1.3 When rejected, a promise: 2.1.3.1 must not transition to any other state. 2.1.3.2 must have a reason, which must not change.
thenable
物件是一類具有then
方法的物件或者函式。
1.2 “thenable” is an object or function that defines a then method.
- 每個 Promise 內部都有一個
value
值,這個value
值可以是任意合法的 JavaScript 資料型別。
1.3 “value” is any legal JavaScript value (including undefined, a thenable, or a promise).
- 除了
value
屬性,Promise 內部還有一個reason
rejected
的原因
1.5 “reason” is a value that indicates why a promise was rejected.
構造 MyPromise 類
根據上面的介紹,可以初步構造一個 MyPromise
類:
class MyPromise {
constructor(exector) {
this.status = MyPromise.PENDING;
this.value = null;
this.reason = null;
this.initBind();
this.init(exector);
}
initBind() {
// 繫結 this
// 因為 resolve 和 reject 會在 exector 作用域中執行,因此這裡需要將 this 繫結到當前的例項
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
init(exector) {
try {
exector(this.resolve, this.reject);
} catch (err) {
this.reject(err);
}
}
resolve(value) {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.FULFILLED;
this.value = value;
}
}
reject(reason) {
if (this.status === MyPromise.PENDING) {
this.status = MyPromise.REJECTED;
this.reason = reason;
}
}
}
// 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending"
MyPromise.FULFILLED = "fulfilled"
MyPromise.REJECTED = "rejected"
複製程式碼
exector
是建立 Promise 物件時傳遞給建構函式的引數,resolve
和 reject
方法分別用來將 Promise 物件的狀態由 pending
轉換成 fulfilled
和 rejected
,並向 Promise 物件中寫入相應的 value
或者 reason
值。 現在,我們可以對上面的程式碼進行一些測試:
const p1 = new MyPromise((resolve,reject) => {
const rand = Math.random();
if(rand > 0.5) resolve(rand)
else reject(rand)
})
console.log(p1)
// MyPromise {status: "fulfilled", value: 0.9121690746412516, reason: null, resolve: ƒ, reject: ƒ}
複製程式碼
上面的程式碼,已經可以讓 Promise 物件實現狀態變換,並儲存 value
或者 reason
值,但單純完成狀態的轉換和儲存值是不夠的,作為非同步的解決方案,我們還需要讓 Promise 物件在狀態變換後再做點什麼。 這就需要我們為 Promise 物件再提供一個 then
方法。
A promise must provide a then method to access its current or eventual value or reason.
then 方法
then
方法接受兩個引數:Promise 狀態轉換為 fulfilled
的回撥(成功回撥)和狀態轉換為 rejected
的回撥(失敗回撥),這兩個回撥函式是可選的。
A promise’s then method accepts two arguments: promise.then(onFulfilled, onRejected) 2.2.1 Both onFulfilled and onRejected are optional arguments: 2.2.1.1 If onFulfilled is not a function, it must be ignored. 2.2.1.2 If onRejected is not a function, it must be ignored.
下面為 MyPromise 類新增一個 then
方法:
...
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
onRejected = typeof onRejected === "function" ? onRejected : () => {}
if (this.status === MyPromise.FULFILLED) {
try{
onFulfilled(this.value)
}catch(e){
onRejected(e)
}
}
if (this.status === MyPromise.REJECTED) {
try{
onRejected(this.reason);
}catch(e){
onRejected(e)
}
}
}
...
複製程式碼
下面測試一下 then
方法:
const p1 = new MyPromise((resolve) => resolve("Success"))
p1.then(data => console.log(data))
// Success
複製程式碼
這裡,我們初步完成了 MyPromise 類的 then
方法。 但仔細看上面的 then
方法和 MyPromise 類的實現,還存在幾個缺陷:
- 只處理了狀態為
fulfilled
和rejected
的情況,沒有處理狀態為pending
的情況 onFulfilled
和onRejected
方法是同步執行的,也就是說,呼叫then
方法,就會執行onFulfilled
和onRejected
方法- MyPromise 類中的
resolve
和reject
方法也是同步的,這意味著會出現下面的情況:
console.log("START")
const p2 = new MyPromise(resolve => resolve("RESOLVED"))
console.log(p2.value)
console.log("END")
複製程式碼
輸出結果為:
START
RESOLVED
END
複製程式碼
按照規範,Promise 應該是非同步的。
2.2.4
onFulfilled
oronRejected
must not be called until the execution context stack contains only platform code.
規範還指出了,應該使用 setTimeout
或 setImmediate
這樣的巨集任務方式,或者 MutationObserver
或 process.nextTick
這樣的微任務方式,來呼叫 onFulfilled
和 onRejected
方法。
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that
onFulfilled
andonRejected
execute asynchronously, after the event loop turn in whichthen
is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such assetTimeout
orsetImmediate
, or with a “micro-task” mechanism such asMutationObserver
orprocess.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.
- MyPromise 物件的狀態不能被非同步的改變,換句話說,無法滿足
exector
方法為非同步的情況:
const p3 = new MyPromise(resolve => setTimeout(() => resolve("RESOLVED")));
p3.then(data => console.log(data))
// 無輸出
複製程式碼
這裡無輸出的原因是在實現 then
方法的時候,沒有處理狀態為 pending
的情況,那麼在 pending
狀態下,對於 then
方法的呼叫,不會有任何的響應,因此在 then
方法中,對於 pending
狀態的處理也很重要。 下面就針對上面出現的問題,做一些改進。
改進
首先,應該確保 onFulfilled
和 onRejected
方法,以及 resolve
和 reject
方法是非同步呼叫的:
...
resolve(value) {
if (this.status === MyPromise.PENDING) {
setTimeout(() => {
this.status = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallback.forEach(cb => cb(this.value));
})
}
}
reject(reason) {
if (this.status === MyPromise.PENDING) {
setTimeout(() => {
this.status = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCallback.forEach(cb => cb(this.reason));
})
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
onRejected = typeof onRejected === "function" ? onRejected : () => {}
if (this.status === MyPromise.FULFILLED) {
setTimeout(() => {
try{
onFulfilled(this.value)
}catch(e){
onRejected(e)
}
})
}
if (this.status === MyPromise.REJECTED) {
setTimeout(() => {
try{
onRejected(this.reason);
}catch(e){
onRejected(e)
}
})
}
}
...
複製程式碼
然後,需要在 MyPromise 類中,在設定兩個佇列:onFulfilledCallback
,和 onRejectedCallback
,用來存放在 pending
狀態下,呼叫 then
方法時傳入的回撥函式。 在呼叫 resolve
和 reject
方法時,需要將佇列中存放的回撥按照先後順序依次呼叫(是不是感覺很像瀏覽器的事件環機制)。
class MyPromise {
constructor(exector) {
this.status = MyPromise.PENDING;
this.value = null;
this.reason = null;
/**
* 2.2.6 then may be called multiple times on the same promise
* 2.2.6.1 If/when promise is fulfilled, all respective onFulfilled callbacks must execute in the order of their originating calls to then
* 2.2.6.2 If/when promise is rejected, all respective onRejected callbacks must execute in the order of their originating calls to then.
*/
this.onFulfilledCallback = [];
this.onRejectedCallback = [];
this.initBind();
this.init(exector);
}
initBind() {
// 繫結 this
// 因為 resolve 和 reject 會在 exector 作用域中執行,因此這裡需要將 this 繫結到當前的例項
this.resolve = this.resolve.bind(this);
this.reject = this.reject.bind(this);
}
init(exector) {
try {
exector(this.resolve, this.reject);
} catch (err) {
this.reject(err);
}
}
resolve(value) {
if (this.status === MyPromise.PENDING) {
setTimeout(() => {
this.status = MyPromise.FULFILLED;
this.value = value;
this.onFulfilledCallback.forEach(cb => cb(this.value));
})
}
}
reject(reason) {
if (this.status === MyPromise.PENDING) {
setTimeout(() => {
this.status = MyPromise.REJECTED;
this.reason = reason;
this.onRejectedCallback.forEach(cb => cb(this.reason));
})
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
onRejected = typeof onRejected === "function" ? onRejected : () => {}
if (this.status === MyPromise.FULFILLED) {
setTimeout(() => {
try{
onFulfilled(this.value)
}catch(e){
onRejected(e)
}
})
}
if (this.status === MyPromise.REJECTED) {
setTimeout(() => {
try{
onRejected(this.reason);
}catch(e){
onRejected(e)
}
})
}
if (this.status === MyPromise.PENDING) {
// 向對了中裝入 onFulfilled 和 onRejected 函式
this.onFulfilledCallback.push((value) => {
try{
onFulfilled(value)
}catch(e){
onRejected(e)
}
})
this.onRejectedCallback.push((reason) => {
try{
onRejected(reason)
}catch(e){
onRejected(e)
}
})
}
}
}
// 2.1 A promise must be in one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending"
MyPromise.FULFILLED = "fulfilled"
MyPromise.REJECTED = "rejected"
複製程式碼
進行一些測試:
console.log("===START===")
const p4 = new MyPromise(resolve => setTimeout(() => resolve("RESOLVED")));
p4.then(data => console.log(1,data))
p4.then(data => console.log(2,data))
p4.then(data => console.log(3,data))
console.log("===END===")
複製程式碼
輸出結果:
===START===
===END===
1 'RESOLVED'
2 'RESOLVED'
3 'RESOLVED'
複製程式碼
實現鏈式呼叫
規範還規定,then
方法必須返回一個新的 Promise 物件,以實現鏈式呼叫。
2.2.7 then must return a promise. promise2 = promise1.then(onFulfilled, onRejected);
如果 onFulfilled
和 onRejected
是函式,就用函式呼叫的返回值,來改變新返回的 promise2
物件的狀態。
2.2.7.1 If either
onFulfilled
oronRejected
returns a valuex
, run the Promise Resolution Procedure[[Resolve]](promise2, x)
. 2.2.7.2 If eitheronFulfilled
oronRejected
throws an exceptione
,promise2
must be rejected withe
as the reason.
這裡提到的 Promise Resolution Procedure,其實是針對 onFulfilled
和 onRejected
方法不同返回值的情況,來對 promise2 的狀態來統一進行處理,我們暫時先忽略,後文再提供實現。
另外,如果 onFulfilled
和 onRejected
不是函式,那麼就根據當前 promise 物件(promise1)的狀態,來改變 promise2 的狀態。
2.2.7.3 If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1. 2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.
由於在前面的程式碼,已對 onFulfilled
和 onRejected
函式進行來處理,如果不是函式的話,提供一個預設值:
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
onRejected = typeof onRejected === "function" ? onRejected : () => {}
複製程式碼
並且每次呼叫 onFulfilled
或 onRejected
方法時,都會傳入當前例項的 value
或者 reason
屬性,因此對於 onFulfilled
或 onRejected
不是函式的特殊情況,直接將傳給它們的引數返回即可,promise2 依舊使用 onFulfilled
或 onRejected
的返回值來改變狀態:
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason }
複製程式碼
上面的方案,還順帶解決了值穿透的問題。所謂值穿透,就是呼叫 then
方法時,如果不傳入引數,下層鏈條中的 then
方法還能夠正常的獲取到 value
或者 reason
值。
new MyPromise(resolve => setTimeout(() => { resolve("Success") }))
.then()
.then()
.then()
...
.then(data => console.log(data));
複製程式碼
下面就根據上面的陳述,對 then
方法做進一步的改進:
···
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason }
let promise2;
if (this.status === MyPromise.FULFILLED) {
return promise2 = new MyPromise((resolve,reject) => {
setTimeout(() => {
try{
const x = onFulfilled(this.value)
resolve(x);
}catch(e){
reject(e)
}
})
})
}
if (this.status === MyPromise.REJECTED) {
return promise2 = new MyPromise((resolve,reject) => {
setTimeout(() => {
try{
const x = onRejected(this.reason)
resolve(x);
}catch(e){
reject(e)
}
})
})
}
if (this.status === MyPromise.PENDING) {
return promise2 = new MyPromise((resolve,reject) => {
// 向對了中裝入 onFulfilled 和 onRejected 函式
this.onFulfilledCallback.push((value) => {
try{
const x = onFulfilled(value)
resolve(x)
}catch(e){
reject(e)
}
})
this.onRejectedCallback.push((reason) => {
try{
const x = onRejected(reason)
resolve(x);
}catch(e){
reject(e)
}
})
})
}
}
···
複製程式碼
規範規定,then
方法必須返回一個新的 Promise 物件(promise2),新的 promise2 的狀態必須依賴於呼叫 then
方法的 Promise 物件(promise1)的狀態,也就是說,必須要等到 promise1 的狀態變成 fulfilled
或者 rejected
之後,promise2 的狀態才能進行改變。 因此,在 then
方法的實現中,在當前的 Promise 物件(promise1)的狀態為 pending
時,將改變 promise2
狀態的方法加入到回撥函式的佇列中。
實現 resolvePromise 方法
上面的程式碼,處理了 onFulfilled
和 onRejected
方法的返回值的情況,以及實現了 then
方法的鏈式呼叫。 現在考慮一個問題,如果 onFulfilled
或 onRejected
方法返回的是一個 Promise 物件,或者是具有 then
方法的其他物件(thenable
物件),該怎麼處理呢? 規範中提到,對於 onFulfilled
或 onRejected
的返回值的,提供一個 Promise Resolution Procedure 方法進行統一的處理,以適應不同的返回值型別。
2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).
我們將這個方法命名為 resolvePromise
方法,將其設計為 MyPromise 類上的一個靜態方法。 resolvePromise
靜態方法的作用,就是根據 onFulfilled
或 onRejected
不同的返回值(x
)的情況,來改變 then
方法返回的 Promise 物件的狀態。 可以這樣理解:我們將改變 promise2 物件的狀態的過程,移動到了 resolvePromise
方法中,以便處理更多的細節問題。 下面是 resolvePromise
方法的實現:
MyPromise.resolvePromise = (promise2,x,resolve,reject) => {
let called = false;
/**
* 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
*/
if(promise2 === x){
return reject(new TypeError("cannot return the same promise object from onfulfilled or on rejected callback."))
}
if(x instanceof MyPromise){
// 處理返回值是 Promise 物件的情況
/**
* new MyPromise(resolve => {
* resolve("Success")
* }).then(data => {
* return new MyPromise(resolve => {
* resolve("Success2")
* })
* })
*/
if(x.status === MyPromise.PENDING){
/**
* 2.3.2.1 If x is pending, promise must remain pending until x is fulfilled or rejected.
*/
x.then(y => {
// 用 x 的 fulfilled 後的 value 值 y,去設定 promise2 的狀態
// 上面的注視,展示了返回 Promise 物件的情況,這裡呼叫 then 方法的原因
// 就是通過引數 y 或者 reason,獲取到 x 中的 value/reason
// 拿到 y 的值後,使用 y 的值來改變 promise2 的狀態
// 依照上例,上面生成的 Promise 物件,其 value 應該是 Success2
// 這個 y 值,也有可能是新的 Promise,因此要遞迴的進行解析,例如下面這種情況
/**
* new Promise(resolve => {
* resolve("Success")
* }).then(data => {
* return new Promise(resolve => {
* resolve(new Promise(resolve => {
* resolve("Success3")
* }))
* })
* }).then(data => console.log(data))
*/
// 總之,使用 “return”鏈中最後一個 Promise 物件的狀態,來決定 promise2 的狀態
MyPromise.resolvePromise(promise2, y, resolve, reject)
},reason => {
reject(reason)
})
}else{
/**
* 2.3 If x is a thenable, it attempts to make promise adopt the state of x,
* under the assumption that x behaves at least somewhat like a promise.
*
* 2.3.2 If x is a promise, adopt its state [3.4]:
* 2.3.2.2 If/when x is fulfilled, fulfill promise with the same value.
* 2.3.2.4 If/when x is rejected, reject promise with the same reason.
*/
x.then(resolve,reject)
}
/**
* 2.3.3 Otherwise, if x is an object or function,
*/
}else if((x !== null && typeof x === "object") || typeof x === "function"){
/**
* 2.3.3.1 Let then be x.then.
* 2.3.3.2 If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
*/
try{
// then 方法可能設定了訪問限制(setter),因此這裡進行了錯誤捕獲處理
const then = x.then;
if(typeof then === "function"){
/**
* 2.3.3.2 If retrieving the property x.then results in a thrown exception e,
* reject promise with e as the reason.
*/
/**
* 2.3.3.3.1 If/when resolvePromise is called with a value y, run [[Resolve]](promise, y).
* 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r.
*/
then.call(x,y => {
/**
* If both resolvePromise and rejectPromise are called,
* or multiple calls to the same argument are made,
* the first call takes precedence, and any further calls are ignored.
*/
if(called) return;
called = true;
MyPromise.resolvePromise(promise2, y, resolve, reject)
},r => {
if(called) return;
called = true;
reject(r);
})
}else{
resolve(x)
}
}catch(e){
/**
* 2.3.3.3.4 If calling then throws an exception e,
* 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, ignore it.
* 2.3.3.3.4.2 Otherwise, reject promise with e as the reason.
*/
if(called) return;
called = true;
reject(e)
}
}else{
// If x is not an object or function, fulfill promise with x.
resolve(x);
}
}
複製程式碼
在我實現規範的規程中,這個 resolvePromise
最最難理解的,主要是 return 鏈這裡,因為想不到具體的場景。我將具體的場景通過註釋的方式寫在上面的程式碼中了,同樣迷惑的童鞋可以看看。
進行 promises-aplus-tests 測試
通過 promises-aplus-tests 可以測試我們實現的 Promise 類是否滿足 Promise/A+ 規範。 進行測試之前,需要為 promises-aplus-tests 提供一個 deferred
的鉤子:
MyPromise.deferred = function() {
const defer = {}
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve
defer.reject = reject
})
return defer
}
try {
module.exports = MyPromise
} catch (e) {
}
複製程式碼
安裝並執行測試:
npm install promises-aplus-tests -D
npx promises-aplus-tests promise.js
複製程式碼
測試結果如下,全部通過:
至此,我們實現了一個完全滿足 Promise/A+ 規範的 Promise,本文的程式碼倉庫在 這裡,歡迎 Star~。完。