ES6+ 實現一個簡版的 Promise
1. 前言
上一節我們學習了 ES6 Promise的基本用法,并且我們知道 Promise 最早出現在社區,所以ES6 中 Promise 也是遵循一個標準的規范的。這個規范就是 Promise A+ 規范 也就是任何人都可以遵循這個規范實現一個自己的 Promise,由于每個人實現的方式有所差異,Promise A+ 規范 給出了一些要求和兼容方式。
本節我們將根據 Promise A+ 規范 實現一個簡版的 Promise API。
2. 實現步驟
上一節我們已經知道了 Promise 是一個類,默認接收一個參數 executor(執行器),并且會立即執行。所以首先需要創建一個 Promise 的類,然后傳入一個回調函數并執行它,故有如下的初始代碼:
class Promise {
constructor(executor) {
executor();
}
}
Promise 有三個狀態:等待(padding)、成功(fulfilled),失?。╮ejected)。默認是等待狀態,等待態可以突變為成功態或失敗態,所以我們可以定義三個常量來存放這三個狀態
const PENDING = 'PENDING';
const RESOLVED = 'RESOLVED'; // 成功態
const REJECTED = 'REJECTED'; // 失敗態
class Promise {
constructor(executor) {
this.status = PENDING; // 默認是等待態
executor();
}
}
這樣我們就知道了 Promise 的基本狀態,那內部的狀態是怎么突變為成功或失敗的呢?這里執行器(executor)會提供兩個個方法用于改變 Promise 的狀態,所以我們需要在初始化時定義 resolve 和 reject 方法:在成功的時候會傳入成功的值,在失敗的時候會傳入失敗的原因。并且每個Promise 都會提供 then方法用于鏈式調用。
class Promise {
constructor(executor) {
this.status = PENDING;
const resolve = (value) => {};
const reject = (reason) => {};
// 執行executor時,傳入成功或失敗的回調
executor(resolve, reject);
}
then(onfulfilled, onrejected) {
}
}
這時我們就可以開始著手去更改 Promise的狀態了,由于默認情況下 Promise 的狀態只能從 pending 到 fulfilled 和 rejected 的轉化。
class Promise {
constructor(executor) {
this.status = PENDING;
const resolve = (value) => {
// 只有等待態時才能更改狀態
if (this.status === PENDING) {
this.status = RESOLVED;
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.status = REJECTED;
}
};
executor(resolve, reject);
}
...
}
成功和失敗都會返回對應的結果,所以我們需要定義成功的值和失敗的原因兩個全局變量,用于存放返回的結果。
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = (value) => {
// 只有等待態時才能更改狀態
if (this.status === PENDING) {
this.value = value;
this.status = RESOLVED;
}
};
const reject = (reason) => {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED;
}
};
executor(resolve, reject);
}
...
}
這時我們就已經為執行器提供了兩個回調函數了,如果在執行器執行時拋出異常時,我們需要使用 try…catch 來補貨一下。由于是拋出異常,所以,需要調用 reject 方法來修改為失敗的狀態。
try {
executor(resolve, reject);
} catch(e) {
reject(e)
}
我們知道實例在調用 then 方法時會傳入兩個回調函數 onfulfilled, onrejected 去執行成功或失敗的回調,所以根據狀態會調用對應的函數來處理。
then(onfulfilled, onrejected) {
if (this.status === RESOLVED) {
onfulfilled(this.value)
}
if (this.status === REJECTED) {
onrejected(this.reason)
}
}
這樣我們就完了 Promise 最基本的同步功能,
let promise = new Promise((resolve, reject) => {
resolve('value');
// throw new Error('錯誤');
// reject('error reason')
// setTimeout(() => {
// resolve('value');
// }, 1000)
})
promise.then((data) => {
console.log('resolve response', data);
}, (err) => {
console.log('reject response', err);
})
用上面的代碼對我們寫的 Promise 進行驗證,通過測試用例可知,我們寫的 Promise 只能在同步中運行,當我們使用 setTimeout 異步去返回時,并沒有預想的在then的成功回調中打印結果。
對于這種異步行為需要專門處理,如何處理異步的內容呢?我們知道在執行異步任務時 Promise 的狀態并沒有被改變,也就是并沒有執行 resolve 或 reject 方法,但是 then 中的回調已經執行了,這時就需要增加當 Promise 還是等待態的邏輯,在等待態時把回調函數都存放起來,等到執行 resolve 或 reject 再依次執行之前存放的then的回調函數,也就是我們平時用到的發布訂閱模式。實現步驟:
- 首先,需要在初始化中增加存放成功的回調函數和存放失敗的回調函數;
- 然后,由于是異步執行 resolve 或 reject 所以需要在 then 方法中把回調函數存放起來;
- 最后,當執行 resolve 或 reject 時取出存放的回調函數依次執行。
根據以上的實現步驟可以得到如下的代碼:
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined; // 成功的值
this.reason = undefined; // 失敗的原因
+ // 存放成功的回調函數
+ this.onResolvedCallbacks = [];
+ // 存放失敗的回調函數
+ this.onRejectedCallbacks = [];
let resolve = (value) => {
if (this.status === PENDING) {
this.value = value;
this.status = RESOLVED;
+ // 異步時,存放在成功的回調函數依次執行
+ this.onResolvedCallbacks.forEach(fn => fn())
}
};
let reject = (reason) => {
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
+ // 異步時,存放在失敗的回調函數依次執行
+ this.onRejectedCallbacks.forEach(fn => fn())
}
};
try {
executor(resolve, reject);
} catch(e) {
reject(e)
}
}
then(onfulfilled, onrejected) {
if (this.status === RESOLVED) {
onfulfilled(this.value)
}
if (this.status === REJECTED) {
onrejected(this.reason)
}
+ if (this.status === PENDING) {
+ this.onResolvedCallbacks.push(() => {
+ // TODO
+ onfulfilled(this.value);
+ })
+ this.onRejectedCallbacks.push(() => {
+ // TODO
+ onrejected(this.reason);
+ })
+ }
}
}
上面的代碼中,在存放回調函數時把 onfulfilled
, onrejected
存放在一個函數中執行,這樣的好處是可以在前面增加處理問題的邏輯。這樣我們就完成了處理異步的 Promise 邏輯。下面是測試用例,可以正常的執行 then 的成功回調函數。
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('100');
}, 1000)
})
promise.then((data) => {
console.log('resolve response:', data); // resolve response: 100
}, (err) => {
console.log('reject response:', err);
})
到這里我們是不是已經基本實現了 Promise 的功能呢?ES6 中的 then 方法支持鏈式調用,那我們寫的可以嗎?我們在看下面的一個測試用例:
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('100');
}, 1000)
})
promise.then((data) => {
console.log('resolve response:', data); // resolve response: 100
return 200
}, (err) => {
console.log('reject response:', err);
}).then((data) => {
console.log('data2:', data)
}, null)
// TypeError: Cannot read property 'then' of undefined
然而當我們在執行的時候會報錯,then 是 undefined。為什么會這樣呢?那我們要知道如何滿足鏈式調用的規范,那就是在完成任務后再返回一個Promise 實例。那如何返回一個 Promise 實例呢?在 Promise A+ 規范的 2.2.7 小節在有詳細的描述,再實例化一個 promise2 來存放執行后的結果,并返回 promise2。那么我們就要改造 then 方法了。
class Promise {
...
then(onfulfilled, onrejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === RESOLVED) {
const x = onfulfilled(this.value)
resolve(x)
}
if (this.status === REJECTED) {
const x = onrejected(this.reason);
reject(x)
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
const x = onfulfilled(this.value)
resolve(x)
})
this.onRejectedCallbacks.push(() => {
const x = onrejected(this.reason);
reject(x)
})
}
})
return promise2
}
}
再使用上面的測試用例,就可以得到正確的結果:
let promise = new Promise((resolve, reject) => {
resolve('100');
})
promise.then((data) => {
console.log('data1:', data); // data1: 100
return 200
}, null).then((data) => {
console.log('data2:', data); // data2: 200
throw new Error('error')
}, null).then(null, () => {
consol.log('程序報錯...')
})
上面的測試用例中,當 then 的回調函數拋出異常時需要去捕獲錯誤,傳到下一個 then 的失敗回調函數中。
class Promise {
...
then(onfulfilled, onrejected) {
let promise2 = new Promise((resolve, reject) => {
if (this.status === RESOLVED) {
try{
const x = onfulfilled(this.value)
resolve(x)
} catch(e) {
reject(e)
}
}
if (this.status === REJECTED) {
try{
const x = onrejected(this.reason);
resolve(x)
} catch(e) {
reject(e)
}
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
try{
const x = onfulfilled(this.value)
resolve(x)
} catch(e) {
reject(e)
}
})
this.onRejectedCallbacks.push(() => {
try{
const x = onrejected(this.reason);
resolve(x)
} catch(e) {
reject(e)
}
})
}
})
return promise2
}
}
到這里為止我們就已經實現了一個簡版的 Promise,因為Promise是一個規范,很多人都可以實現自己的 Promise 所以 Promise A+ 規范做了很多兼容處理的要求,如果想實現一個完整的 Promise 可以參考 Promise A+ 規范 。
3. 小結
本節主要按照 Promise A+ 規范 部分的要求實現了一個簡版的 Promise API 這個 Promise 基本上滿足同步異步的鏈式調用,對基本的異常做了處理。當然 Promise A+ 規范 所規定的細節比較多,剩下的都是對各種異常錯誤的處理,所以后面我們也沒有去實現。另外官網下提供了一個測試用例來驗證我們寫的 Promise 是否符合 Promise A+ 規范 ,所以可以參考 promises-tests 這個庫來完成我們的 Promise 的測試。