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