前言
都 2020 年了,Promise 大家肯定都在用了,但是估計很多人對其原理還是一知半解,今天就讓我們一起實現(xiàn)一個符合 PromiseA+ 規(guī)范的 Promise。
簡單版
我們都知道 Promise 的調(diào)用方式,new Promise(executor), executor 兩個參數(shù),resolve,reject。所以現(xiàn)在我們的代碼長這樣
class Promise {
constructor(executor) {
const resolve = () => {}
const reject = () => {}
executor(resolve, rejcet)
}
}
Promise 內(nèi)部有三個狀態(tài),pending、fulfilled、rejected,初始是 pending,調(diào)用 resolve 后變?yōu)?fulfilled,,調(diào)用 reject 后變?yōu)?rejected。fulfilled 時會調(diào)用 then 注冊的成功的回調(diào),rejected 時會調(diào)用 then 注冊的失敗的回調(diào)。
// Promise 內(nèi)部狀態(tài)
const STATUS = { PENDING: 'PENDING', FUFILLED: 'FUFILLED', REJECTED: 'REJECTED' }
class Promise {
constructor(executor) {
this.status = STATUS.PENDING;
this.value = undefined; // 成過的值
this.reason = undefined; // 失敗的值
const resolve = (val) => {
if (this.status == STATUS.PENDING) {
this.status = STATUS.FUFILLED;
this.value = val;
}
}
const reject = (reason) => {
if (this.status == STATUS.PENDING) {
this.status = STATUS.REJECTED;
this.reason = reason;
}
}
try {
executor(resolve, reject);
} catch (e) {
// 出錯走失敗邏輯
reject(e)
}
}
then(onFulfilled, onRejected) {
if (this.status == STATUS.FUFILLED) {
onFulfilled(this.value);
}
if (this.status == STATUS.REJECTED) {
onRejected(this.reason);
}
}
}
現(xiàn)在我們的 Promise 已經(jīng)初步實現(xiàn)了,但還有很多問題
一個 promise 可以調(diào)用多次 then 方法,也就是說可以注冊多個回調(diào),所以我們需要一個隊列來保存這些回調(diào)。同時我們沒有對 pending 狀態(tài)的 then 方法做處理,當(dāng) promise 為 pending 狀態(tài)時,then 方法應(yīng)該將回調(diào)放入到隊列當(dāng)中,而不是直接運行。所以改進之后的代碼如下。
const STATUS = { PENDING: 'PENDING', FUFILLED: 'FUFILLED', REJECTED: 'REJECTED' }
class Promise {
constructor(executor) {
this.status = STATUS.PENDING;
this.value = undefined; // 成過的值
this.reason = undefined; // 失敗的值
+ this.onResolvedCallbacks = []; // 存放成功的回調(diào)的
+ this.onRejectedCallbacks = []; // 存放失敗的回調(diào)的
const resolve = (val) => {
if (this.status == STATUS.PENDING) {
this.status = STATUS.FUFILLED;
this.value = val;
// 成功時調(diào)用成功隊列里的回調(diào)
+ this.onResolvedCallbacks.forEach(fn=>fn());
}
}
const reject = (reason) => {
if (this.status == STATUS.PENDING) {
this.status = STATUS.REJECTED;
this.reason = reason;
// 失敗時調(diào)用失敗隊列里的回調(diào)
+ this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try {
executor(resolve, reject);
} catch (e) {
// 出錯走失敗邏輯
reject(e)
}
}
then(onFulfilled, onRejected) {
if (this.status === STATUS.FUFILLED) {
onFulfilled(this.value);
}
if (this.status === STATUS.REJECTED) {
onRejected(this.reason);
}
+ if (this.status === STATUS.PENDING) {
+ this.onResolvedCallbacks.push(()=>{ // todo..
+ onFulfilled(this.value);
+ })
+ this.onRejectedCallbacks.push(()=>{ // todo..
+ onRejected(this.reason);
+ })
+ }
+ }
}
到這一個簡單 promise 80%的功能已經(jīng)實現(xiàn)了,但是還有一個問題,promise 可以鏈?zhǔn)秸{(diào)用,也就是我們常看到的 promise.then().then()。所以我們得在 then 方法里去返回一個新的 promise。
const STATUS = { PENDING: 'PENDING', FUFILLED: 'FUFILLED', REJECTED: 'REJECTED' }
class Promise {
// 上面邏輯省略
...
then(onFulfilled, onRejected) { // swtich 作用域
+ let promise2 = new Promise((resolve, reject) => {
+ if (this.status === STATUS.FUFILLED) {
+ // to....
+ try {
+ let x = onFulfilled(this.value);
+ resolve(x);
+ } catch (e) {
+ reject(e);
+ }
+ }
+ if (this.status === STATUS.REJECTED) {
+ try {
+ let x = onRejected(this.reason);
+ resolve(x);
+ } catch (e) {
+ reject(e);
+ }
+ }
+ if (this.status === STATUS.PENDING) {
+ this.onResolvedCallbacks.push(() => { // todo..
+ try {
+ let x = onFulfilled(this.value);
+ resolve(x);
+ } catch (e) {
+ reject(e);
+ }
+ })
+ this.onRejectedCallbacks.push(() => { // todo..
+ try {
+ let x = onRejected(this.reason);
+ resolve(x);
+ } catch (e) {
+ reject(e);
+ }
+
+ })
+ }
+ })
+
+ return promise2;
+ }
}
我們注意到我們把回調(diào)的執(zhí)行邏輯都放到了 promise2 的內(nèi)部,之所以這樣做,是因為我們需要用 onFufilled 的返回值去 resolve promise2,這也是為什么 then 回調(diào)的返回值會傳給下一個 then 的原因。
完整版
上面的 promise 與規(guī)范有一些差距
then 注冊的回調(diào)都是異步執(zhí)行的
如果 then 注冊回調(diào)的返回值是個函數(shù)或?qū)ο?,這里處理起來會復(fù)雜一點,我們先看看規(guī)范是怎么定義的
promise2 = promise1.then(onFulfilled, onRejected);
x = onFulfilled 或 onRejected 的返回值
- 2.3.1 如果promise和x引用同一個對象,則用TypeError作為原因拒絕(reject)promise。
- 2.3.2 如果x是一個promise,采用promise的狀態(tài)
- 2.3.2.1 如果x是請求狀態(tài)(pending),promise必須保持pending直到xfulfilled或rejected
- 2.3.2.2 如果x是完成態(tài)(fulfilled),用相同的值完成fulfillpromise
- 2.3.2.2 如果x是拒絕態(tài)(rejected),用相同的原因rejectpromise
- 2.3.3另外,如果x是個對象或者方法
- 2.3.3.1 讓x作為x.then.
- 2.3.3.2 如果取回的x.then屬性的結(jié)果為一個異常e,用e作為原因reject promise
- 2.3.3.3 如果then是一個方法,把x當(dāng)作this來調(diào)用它,第一個參數(shù)為resolvePromise,第二個參數(shù)為rejectPromise,其中:
- 2.3.3.3.1 如果/當(dāng) resolvePromise 被一個值 y 調(diào)用,運行 [[Resolve]](promise, y)
- 2.3.3.3.2 如果/當(dāng) rejectPromise 被一個原因 r 調(diào)用,用 r 拒絕(reject)promise
- 2.3.3.3.3 如果 resolvePromise 和 rejectPromise 都被調(diào)用,或者對同一個參數(shù)進行多次調(diào)用,第一次調(diào)用執(zhí)行,任何進一步的調(diào)用都被忽略
- 2.3.3.3.4 如果調(diào)用 then 拋出一個異常 e
- 2.3.3.3.4.1 如果 resolvePromise 或 rejectPromise 已被調(diào)用,忽略。
- 2.3.3.3.4.2 或者, 用 e 作為reason拒絕(reject)promise
規(guī)范可能有點復(fù)雜,需要自己慢慢消化,這里我直接把代碼貼出來,我會在代碼里標(biāo)注每個規(guī)范的實現(xiàn)點。
const STATUS = { PENDING: 'PENDING', FUFILLED: 'FUFILLED', REJECTED: 'REJECTED' }
// 我們的promise 按照規(guī)范來寫 就可以和別人的promise公用
function resolvePromise(x, promise2, resolve, reject) {
// 規(guī)范 2.3.1
if (promise2 == x) { // 防止自己等待自己完成
return reject(new TypeError('出錯了'))
}
// 規(guī)范 2.3.3
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// x可以是一個對象 或者是函數(shù)
let called;
try {
// 規(guī)范 2.3.3.1
let then = x.then;
if (typeof then == 'function') {
// 2.3.3.3
then.call(x, function(y) {
// 規(guī)范 2.3.3.3.3
if (called) return
called = true;
// 規(guī)范 2.3.3.3.1
resolvePromise(y, promise2, resolve, reject);
}, function(r) {
// 規(guī)范 2.3.3.3.3
if (called) return
called = true;
// 規(guī)范 2.3.3.3.2
reject(r);
})
} else {
resolve(x); // 此時x 就是一個普通對象
}
} catch (e) {
// 規(guī)范 2.3.3.3.4.1
if (called) return
called = true;
// 規(guī)范 2.3.3.3.4
reject(e); // 取then時拋出錯誤了
}
} else {
resolve(x); // x是一個原始數(shù)據(jù)類型 不能是promise
}
// 不是proimise 直接就調(diào)用resolve
}
class Promise {
constructor(executor) {
this.status = STATUS.PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = []; // 存放成功的回調(diào)的
this.onRejectedCallbacks = []; // 存放失敗的回調(diào)的
const resolve = (val) => {
if(val instanceof Promise){ // 是promise 就繼續(xù)遞歸解析
return val.then(resolve,reject)
}
if (this.status == STATUS.PENDING) {
this.status = STATUS.FUFILLED;
this.value = val;
// 發(fā)布
this.onResolvedCallbacks.forEach(fn => fn());
}
}
const reject = (reason) => {
if (this.status == STATUS.PENDING) {
this.status = STATUS.REJECTED;
this.reason = reason;
// 腹部
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject);
} catch (e) {
// 出錯走失敗邏輯
reject(e)
}
}
then(onFulfilled, onRejected) { // swtich 作用域
// 可選參數(shù)
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : x => x
onRejected = typeof onRejected === 'function'? onRejected: err=> {throw err}
let promise2 = new Promise((resolve, reject) => {
if (this.status === STATUS.FUFILLED) {
// to....
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(x, promise2, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === STATUS.REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(x, promise2, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
}
if (this.status === STATUS.PENDING) {
// 裝飾模式 切片編程
this.onResolvedCallbacks.push(() => { // todo..
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(x, promise2, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
})
this.onRejectedCallbacks.push(() => { // todo..
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(x, promise2, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
})
}
});
return promise2;
}
}
測試工具
給大家推薦一個測試 promise 是否規(guī)范的工具 --- promises-aplus-tests,使用方法如下
全局安裝 promises-aplus-tests,然后添加以下代碼
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject
})
return dfd;
}
module.exports = Promise
然后直接在在控制臺運行 promises-aplus-tests <當(dāng)前 promise 代碼地址>
可以看到我們的 promise 是順利通過測試的。

總結(jié)
希望這篇文章可以幫助大家更深入的理解 promise
碼字不易,期望得到你的贊。