
前言
關(guān)于 Promise 原理解析的優(yōu)秀文章,在掘金上已經(jīng)有非常多了。但是筆者總是處在 看了就會,一寫就廢 的狀態(tài),這是筆者寫這篇文章的目的,為了理一下 Promise 的編寫思路,從零開始手寫一波代碼,同時也方便自己日后回顧。
?
Promise 的作用
Promise 是 JavaScript 異步編程的一種流行解決方案,它的出現(xiàn)是為了解決 回調(diào)地獄 的問題,讓使用者可以通過鏈式的寫法去編寫寫異步代碼,具體的用法筆者就不介紹了,大家可以參考阮一峰老師的 ES6 Promise教程。
?
課前知識
觀察者模式
什么是觀察者模式:
觀察者模式定義了一種一對多的依賴關(guān)系,讓多個觀察者對象同時監(jiān)聽某一個目標對象,當這個目標對象的狀態(tài)發(fā)生變化時,會通知所有觀察者對象,使它們能夠自動更新。
Promise 是基于 觀察者的設(shè)計模式 實現(xiàn)的,then 函數(shù)要執(zhí)行的函數(shù)會被塞入觀察者數(shù)組中,當 Promise 狀態(tài)變化的時候,就去執(zhí)行觀察組數(shù)組中的所有函數(shù)。
事件循環(huán)機制
實現(xiàn) Promise 涉及到了 JavaScript 中的事件循環(huán)機制 EventLoop、以及宏任務和微任務的概念。
事件循環(huán)機制的流程圖如下:
大家可以看一下這段代碼:
console.log(1);
setTimeout(() => {
console.log(2);
},0);
let a = new Promise((resolve) => {
console.log(3);
resolve();
}).then(() => {
console.log(4);
}).then(() => {
console.log(5);
});
console.log(6);
如果不能一下子說出輸出結(jié)果,建議大家可以先查閱一下 事件循環(huán) 的相關(guān)資料,在掘金中有很多優(yōu)秀的文章。
Promises/A+ 規(guī)范
Promises/A+ 是一個社區(qū)規(guī)范,如果你想寫出一個規(guī)范的 Promise,我們就需要遵循這個標準。之后我們也會根據(jù)規(guī)范來完善我們自己編寫的 Promise。
?
Promise 核心知識點
在動手寫 Promise 之前,我們先過一下幾個重要的知識點。
executor
// 創(chuàng)建 Promise 對象 x1
// 并在 executor 函數(shù)中執(zhí)行業(yè)務邏輯
function executor(resolve, reject){
// 業(yè)務邏輯處理成功結(jié)果
const value = ...;
resolve(value);
// 失敗結(jié)果
// const reason = ...;
// reject(reason);
}
let x1 = new Promise(executor);
首先 Promise 是一個類,它接收一個執(zhí)行函數(shù) executor,它接收兩個參數(shù):resolve 和 reject,這兩個參數(shù)是 Promise 內(nèi)部定義的兩個函數(shù),用來改變狀態(tài)并執(zhí)行對應回調(diào)函數(shù)。
因為
Promise本身是不知道執(zhí)行結(jié)果失敗或者成功,它只是給異步操作提供了一個容器,實際上的控制權(quán)在使用者的手上,使用者可以調(diào)用上面兩個參數(shù)告訴Promise結(jié)果是否成功,同時將業(yè)務邏輯處理結(jié)果(value/reason)作為參數(shù)傳給resolve和reject兩個函數(shù),執(zhí)行回調(diào)。
三個狀態(tài)
Promise 有三個狀態(tài):
-
pending:等待中 -
resolved:已成功 -
rejected:已失敗
在 Promise 的狀態(tài)改變只有兩種可能:從 pending 變?yōu)?resolved 或者從 pending 變?yōu)?rejected,如下圖(引自 Promise 迷你書):
而且需要注意的是一旦狀態(tài)改變,狀態(tài)不會再變了,接下來就一直是這個結(jié)果。也就是說當我們在 executor 函數(shù)中調(diào)用了 resolve 之后,之后調(diào)用 reject 就沒有效果了,反之亦然。
// 并在 executor 函數(shù)中執(zhí)行業(yè)務邏輯
function executor(resolve, reject){
resolve(100);
// 之后調(diào)用 resolve,reject 都是無效的,
// 因為狀態(tài)已經(jīng)變?yōu)?resolved,不會再改變了
reject(100);
}
let x1 = new Promise(executor);
then
每一個 promise 都一個 then 方法,這個是當 promise 返回結(jié)果之后,需要執(zhí)行的回調(diào)函數(shù),他有兩個可選參數(shù):
-
onFulfilled:成功的回調(diào); -
onRejected:失敗的回調(diào);
如下圖(引自 Promise 迷你書):
// ...
let x1 = new Promise(executor);
// x1 延遲綁定回調(diào)函數(shù) onResolve
function onResolved(value){
console.log(value);
}
// x1 延遲綁定回調(diào)函數(shù) onRejected
function onRejected(reason){
console.log(reason);
}
x1.then(onResolved, onRejected);
?
手寫 Promise 大致流程
在這里我們簡單過一下手寫一個 Promise 的大致流程:
executor 與三個狀態(tài)
-
new Promise時,需要傳遞一個executor執(zhí)行器函數(shù),在構(gòu)造函數(shù)中,執(zhí)行器函數(shù)立刻執(zhí)行 -
executor執(zhí)行函數(shù)接受兩個參數(shù),分別是resolve和reject -
Promise只能從pending到rejected, 或者從pending到fulfilled -
Promise的狀態(tài)一旦確認,狀態(tài)就凝固了,不在改變
then 方法
- 所有的
Promise都有then方法,then接收兩個參數(shù),分別是Promise成功的回調(diào)onFulfilled,和失敗的回調(diào)onRejected - 如果調(diào)用
then時,Promise已經(jīng)成功,則執(zhí)行onFulfilled,并將Promise的值作為參數(shù)傳遞進去;如果Promise已經(jīng)失敗,那么執(zhí)行onRejected,并將Promise失敗的原因作為參數(shù)傳遞進去;如果Promise的狀態(tài)是pending,需要將onFulfilled和onRejected函數(shù)存放起來,等待狀態(tài)確定后,再依次將對應的函數(shù)執(zhí)行(觀察者模式) -
then的參數(shù)onFulfilled和onRejected可以不傳,Promise可以進行值穿透。
鏈式調(diào)用并處理 then 返回值
-
Promise可以then多次,Promise的then方法返回一個新的Promise。 - 如果
then返回的是一個正常值,那么就會把這個結(jié)果(value)作為參數(shù),傳遞給下一個then的成功的回調(diào)(onFulfilled) - 如果
then中拋出了異常,那么就會把這個異常(reason)作為參數(shù),傳遞給下一個then的失敗的回調(diào)(onRejected) - 如果
then返回的是一個promise或者其他thenable對象,那么需要等這個promise執(zhí)行完撐,promise如果成功,就走下一個then的成功回調(diào);如果失敗,就走下一個then的失敗回調(diào)。
上面是大致的實現(xiàn)流程,如果迷迷糊糊沒關(guān)系,只要大致有一個印象即可,后續(xù)我們會一一講到。
那接下來我們就開始實現(xiàn)一個最簡單的例子開始講解。
?
第一版(從一個簡單例子開始)
我們先寫一個簡單版,這版暫不支持狀態(tài)、鏈式調(diào)用,并且只支持調(diào)用一個
then方法。
來個 ??
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
p1.then((data) => {
console.log(data);
}, (err) => {
console.log(err);
})
例子很簡單,就是 1s 之后返回 成功了,并在 then 中輸出。
實現(xiàn)
我們定義一個 MyPromise 類,接著我們在其中編寫代碼,具體代碼如下:
class MyPromise {
// ts 接口定義 ...
constructor (executor: executor) {
// 用于保存 resolve 的值
this.value = null;
// 用于保存 reject 的值
this.reason = null;
// 用于保存 then 的成功回調(diào)
this.onFulfilled = null;
// 用于保存 then 的失敗回調(diào)
this.onRejected = null;
// executor 的 resolve 參數(shù)
// 用于改變狀態(tài) 并執(zhí)行 then 中的成功回調(diào)
let resolve = value => {
this.value = value;
this.onFulfilled && this.onFulfilled(this.value);
}
// executor 的 reject 參數(shù)
// 用于改變狀態(tài) 并執(zhí)行 then 中的失敗回調(diào)
let reject = reason => {
this.reason = reason;
this.onRejected && this.onRejected(this.reason);
}
// 執(zhí)行 executor 函數(shù)
// 將我們上面定義的兩個函數(shù)作為參數(shù) 傳入
// 有可能在 執(zhí)行 executor 函數(shù)的時候會出錯,所以需要 try catch 一下
try {
executor(resolve, reject);
} catch(err) {
reject(err);
}
}
// 定義 then 函數(shù)
// 并且將 then 中的參數(shù)復制給 this.onFulfilled 和 this.onRejected
private then(onFulfilled, onRejected) {
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
}
好了,我們的第一版就完成了,是不是很簡單。
不過這里需要注意的是,
resolve函數(shù)的執(zhí)行時機需要在then方法將回調(diào)函數(shù)注冊了之后,在resolve之后在去往賦值回調(diào)函數(shù),其實已經(jīng)完了,沒有任何意義。上面的例子沒有問題,是因為
resolve(成功了)是包在setTimeout中的,他會在下一個宏任務執(zhí)行,這時回調(diào)函數(shù)已經(jīng)注冊了。大家可以試試把
resolve(成功了)從setTimeout中拿出來,這個時候就會出現(xiàn)問題了。
存在問題
這一版實現(xiàn)很簡單,還存在幾個問題:
- 未引入狀態(tài)的概念
未引入狀態(tài)的概念,現(xiàn)在狀態(tài)可以隨意變,不符合 Promise 狀態(tài)只能從等待態(tài)變化的規(guī)則。
- 不支持鏈式調(diào)用
正常情況下我們可以對 Promise 進行鏈式調(diào)用:
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
p1.then(onResolved1, onRejected1).then(onResolved2, onRejected2)
- 只支持一個回調(diào)函數(shù),如果存在多個回調(diào)函數(shù)的話,后面的會覆蓋前面的
在這個例子中,onResolved2 會覆蓋 onResolved1,onRejected2 會覆蓋 onRejected1。
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
// 注冊多個回調(diào)函數(shù)
p1.then(onResolved1, onRejected1);
p1.then(onResolved2, onRejected2);
接下來我們更進一步,把這些問題給解決掉。
?
第二版(實現(xiàn)鏈式調(diào)用)
這一版我們把狀態(tài)的概念引入,同時實現(xiàn)鏈式調(diào)用的功能。
加上狀態(tài)
上面我們說到 Promise 有三個狀態(tài):pending、resovled、rejected,只能從 pending 轉(zhuǎn)為 resovled 或者 rejected,而且當狀態(tài)改變之后,狀態(tài)就不能再改變了。
- 我們定義一個屬性
status:用于記錄當前Promise的狀態(tài) - 為了防止寫錯,我們把狀態(tài)定義成常量
PENDING、RESOLVED、REJECTED。 - 同時我們將保存
then的成功回調(diào)定義為一個數(shù)組:this.resolvedQueues與this.rejectedQueues,我們可以把then中的回調(diào)函數(shù)都塞入對應的數(shù)組中,這樣就能解決我們上面提到的第三個問題。
class MyPromise {
private static PENDING = 'pending';
private static RESOLVED = 'resolved';
private static REJECTED = 'rejected';
constructor (executor: executor) {
this.status = MyPromise.PENDING;
// ...
// 用于保存 then 的成功回調(diào)數(shù)組
this.resolvedQueues = [];
// 用于保存 then 的失敗回調(diào)數(shù)組
this.rejectedQueues = [];
let resolve = value => {
// 當狀態(tài)是 pending 是,將 promise 的狀態(tài)改為成功態(tài)
// 同時遍歷執(zhí)行 成功回調(diào)數(shù)組中的函數(shù),將 value 傳入
if (this.status == MyPromise.PENDING) {
this.value = value;
this.status = MyPromise.RESOLVED;
this.resolvedQueues.forEach(cb => cb(this.value))
}
}
let reject = reason => {
// 當狀態(tài)是 pending 是,將 promise 的狀態(tài)改為失敗態(tài)
// 同時遍歷執(zhí)行 失敗回調(diào)數(shù)組中的函數(shù),將 reason 傳入
if (this.status == MyPromise.PENDING) {
this.reason = reason;
this.status = MyPromise.REJECTED;
this.rejectedQueues.forEach(cb => cb(this.reason))
}
}
try {
executor(resolve, reject);
} catch(err) {
reject(err);
}
}
}
完善 then 函數(shù)
接著我們來完善 then 中的方法,之前我們是直接將 then 的兩個參數(shù) onFulfilled 和 onRejected,直接賦值給了 Promise 的用于保存成功、失敗函數(shù)回調(diào)的實例屬性。
現(xiàn)在我們需要將這兩個屬性塞入到兩個數(shù)組中去:resolvedQueues 和 rejectedQueues。
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
// 首先判斷兩個參數(shù)是否為函數(shù)類型,因為這兩個參數(shù)是可選參數(shù)
// 當參數(shù)不是函數(shù)類型時,需要創(chuàng)建一個函數(shù)賦值給對應的參數(shù)
// 這也就實現(xiàn)了 透傳
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}
// 當狀態(tài)是等待態(tài)的時候,需要將兩個參數(shù)塞入到對應的回調(diào)數(shù)組中
// 當狀態(tài)改變之后,在執(zhí)行回調(diào)函數(shù)中的函數(shù)
if (this.status === MyPromise.PENDING) {
this.resolvedQueues.push(onFulfilled)
this.rejectedQueues.push(onRejected)
}
// 狀態(tài)是成功態(tài),直接就調(diào)用 onFulfilled 函數(shù)
if (this.status === MyPromise.RESOLVED) {
onFulfilled(this.value)
}
// 狀態(tài)是成功態(tài),直接就調(diào)用 onRejected 函數(shù)
if (this.status === MyPromise.REJECTED) {
onRejected(this.reason)
}
}
}
then 函數(shù)的一些說明
- 什么情況下
this.status會是pending狀態(tài),什么情況下會是resolved狀態(tài)
這個其實也和事件循環(huán)機制有關(guān),如下代碼:
// this.status 為 pending 狀態(tài)
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
}).then(value => {
console.log(value)
})
// this.status 為 resolved 狀態(tài)
new MyPromise((resolve, reject) => {
resolve(1)
}).then(value => {
console.log(value)
})
- 什么是 透傳
如下面代碼,當 then 中沒有傳任何參數(shù)的時候,Promise 會使用內(nèi)部默認的定義的方法,將結(jié)果傳遞給下一個 then。
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
p1.then().then((res) => {
console.log(res);
})
因為我們現(xiàn)在還沒支持鏈式調(diào)用,這段代碼運行會出問題。
支持鏈式調(diào)用
支持鏈式調(diào)用,其實很簡單,我們只需要給 then 函數(shù)最后返回 this 就行,這樣就支持了鏈式調(diào)用:
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
// ...
return this;
}
}
每次調(diào)用 then 之后,我們都返回當前的這個 Promise 對象,因為 Promise 對象上是存在 then 方法的,這個時候我們就簡單的實現(xiàn)了 Promise 的簡單調(diào)用。
這個時候運行上面 透傳 的測試代碼了。
但是上面的代碼還是存在相應的問題的,看下面代碼:
const p1 = new MyPromise((resolved, rejected) => {
resolved('resolved');
});
p1.then((res) => {
console.log(res);
return 'then1';
})
.then((res) => {
console.log(res);
return 'then2';
})
.then((res) => {
console.log(res);
return 'then3';
})
// 預測輸出:resolved -> then1 -> then2
// 實際輸出:resolved -> resolved -> resolved
輸出與我們的預期有偏差,因為我們 then 中返回的 this 代表了 p1,在 new MyPromise 之后,其實狀態(tài)已經(jīng)從 pending 態(tài)變?yōu)榱?resolved 態(tài),之后不會再變了,所以在 MyPromise 中的 this.value 值就一直是 resolved。
這個時候我們就得看看關(guān)于 then 返回值的相關(guān)知識點了。
then 返回值
實際上 then 都會返回了一個新的 Promise 對象。
先看下面這段代碼:
// 新創(chuàng)建一個 promise
const aPromise = new Promise(function (resolve) {
resolve(100);
});
// then 返回的 promise
var thenPromise = aPromise.then(function (value) {
console.log(value);
});
console.log(aPromise !== thenPromise); // => true
從上面的代碼中我們可以得出 then 方法返回的 Promise 已經(jīng)不再是最初的 Promise 了,如下圖(引自 Promise 迷你書):
promise的鏈式調(diào)用跟jQuery的鏈式調(diào)用是有區(qū)別的,jQuery鏈式調(diào)用返回的對象還是最初那個jQuery對象;Promise更類似于數(shù)組中一些方法,如slice,每次進行操作之后,都會返回一個新的值。
改造代碼
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason}
// then 方法返回一個新的 promise
const promise2 = new MyPromise((resolve, reject) => {
// 成功狀態(tài),直接 resolve
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數(shù)的返回值,resolve 出去
let x = onFulfilled(this.value);
resolve(x);
}
// 失敗狀態(tài),直接 reject
if (this.status === MyPromise.REJECTED) {
// 將 onRejected 函數(shù)的返回值,reject 出去
let x = onRejected(this.reason)
reject && reject(x);
}
// 等待狀態(tài),將 onFulfilled,onRejected 塞入數(shù)組中,等待回調(diào)執(zhí)行
if (this.status === MyPromise.PENDING) {
this.resolvedQueues.push((value) => {
let x = onFulfilled(value);
resolve(x);
})
this.rejectedQueues.push((reason) => {
let x = onRejected(reason);
reject && reject(x);
})
}
});
return promise2;
}
}
// 輸出結(jié)果 resolved -> then1 -> then2
存在問題
到這里我們就完成了簡單的鏈式調(diào)用,但是只能支持同步的鏈式調(diào)用,如果我們需要在 then 方法中再去進行其他異步操作的話,上面的代碼就 GG 了。
如下代碼:
const p1 = new MyPromise((resolved, rejected) => {
resolved('我 resolved 了');
});
p1.then((res) => {
console.log(res);
return new MyPromise((resolved, rejected) => {
setTimeout(() => {
resolved('then1');
}, 1000)
});
})
.then((res) => {
console.log(res);
return new MyPromise((resolved, rejected) => {
setTimeout(() => {
resolved('then2');
}, 1000)
});
})
.then((res) => {
console.log(res);
return 'then3';
})
上面的代碼會直接將 Promise 對象直接當作參數(shù)傳給下一個 then 函數(shù),而我們其實是想要將這個 Promise 的處理結(jié)果傳遞下去。
?
第三版(異步鏈式調(diào)用)
這一版我們來實現(xiàn)
promise的異步鏈式調(diào)用。
思路
先看一下 then 中 onFulfilled 和 onRejected 返回的值:
// 成功的函數(shù)返回
let x = onFulfilled(this.value);
// 失敗的函數(shù)返回
let x = onRejected(this.reason);
從上面的的問題中可以看出,x 可以是一個 普通值,也可以是一個 Promise 對象,普通值的傳遞我們在 第二版 已經(jīng)解決了,現(xiàn)在需要解決的是當 x 返回一個 Promise 對象的時候該怎么處理。
其實也很簡單,當 x 是一個 Promise 對象的時候,我們需要進行等待,直到返回的 Promise 狀態(tài)變化的時候,再去執(zhí)行之后的 then 函數(shù),代碼如下:
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}
// then 方法返回一個新的 promise
const promise2 = new MyPromise((resolve, reject) => {
// 成功狀態(tài),直接 resolve
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數(shù)的返回值,resolve 出去
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}
// 失敗狀態(tài),直接 reject
if (this.status === MyPromise.REJECTED) {
// 將 onRejected 函數(shù)的返回值,reject 出去
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject);
}
// 等待狀態(tài),將 onFulfilled,onRejected 塞入數(shù)組中,等待回調(diào)執(zhí)行
if (this.status === MyPromise.PENDING) {
this.resolvedQueues.push(() => {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
})
this.rejectedQueues.push(() => {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
})
}
});
return promise2;
}
}
我們新寫一個函數(shù) resolvePromise,這個函數(shù)是用來處理異步鏈式調(diào)用的核心方法,他會去判斷 x 返回值是不是 Promise 對象,如果是的話,就直到 Promise 返回成功之后在再改變狀態(tài),如果是普通值的話,就直接將這個值 resovle 出去:
const resolvePromise = (promise2, x, resolve, reject) => {
if (x instanceof MyPromise) {
const then = x.then;
if (x.status == MyPromise.PENDING) {
then.call(x, y => {
resolvePromise(promise2, y, resolve, reject);
}, err => {
reject(err);
})
} else {
x.then(resolve, reject);
}
} else {
resolve(x);
}
}
代碼說明
resolvePromise
resolvePromise 接受四個參數(shù):
-
promise2是then中返回的promise; -
x是then的兩個參數(shù)onFulfilled或者onRejected的返回值,類型不確定,有可能是普通值,有可能是thenable對象; -
resolve和reject是promise2的。
then 返回值類型
當 x 是 Promise 的時,并且他的狀態(tài)是 Pending 狀態(tài),如果 x 執(zhí)行成功,那么就去遞歸調(diào)用 resolvePromise 這個函數(shù),將 x 執(zhí)行結(jié)果作為 resolvePromise 第二個參數(shù)傳入;
如果執(zhí)行失敗,則直接調(diào)用 promise2 的 reject 方法。
?
到這里我們基本上一個完整的 promise,接下來我們需要根據(jù) Promises/A+ 來規(guī)范一下我們的 Promise。
?
規(guī)范 Promise
前幾版的代碼筆者基本上是按照規(guī)范來的,這里主要講幾個沒有符合規(guī)范的點。
規(guī)范 then(規(guī)范 2.2)
then中onFulfilled和onRejected需要異步執(zhí)行,即放到異步任務中去執(zhí)行(規(guī)范 2.2.4)
實現(xiàn)
我們需要將 then 中的函數(shù)通過 setTimeout 包裹起來,放到一個宏任務中去,這里涉及了 js 的 EventLoop,大家可以去看看相應的文章,如下:
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
// ...
// then 方法返回一個新的 promise
const promise2 = new MyPromise((resolve, reject) => {
// 成功狀態(tài),直接 resolve
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數(shù)的返回值,resolve 出去
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
})
}
// 失敗狀態(tài),直接 reject
if (this.status === MyPromise.REJECTED) {
// 將 onRejected 函數(shù)的返回值,reject 出去
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
})
}
// 等待狀態(tài),將 onFulfilled,onRejected 塞入數(shù)組中,等待回調(diào)執(zhí)行
if (this.status === MyPromise.PENDING) {
this.resolvedQueues.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
})
})
this.rejectedQueues.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
})
})
}
});
return promise2;
}
}
使用微任務包裹
但這樣還是有一個問題,我們知道其實 Promise.then 是屬于微任務的,現(xiàn)在當使用 setTimeout 包裹之后,就相當于會變成一個宏任務,可以看下面這一個例子:
var p1 = new MyPromise((resolved, rejected) => {
resolved('resolved');
})
setTimeout(() => {
console.log('---setTimeout---');
}, 0);
p1.then(res => {
console.log('---then---');
})
// 正常 Promise:then -> setTimeout
// 我們的 Promise:setTimeout -> then
輸出順序不一樣,原因是因為現(xiàn)在的 Promise 是通過 setTimeout 宏任務包裹的。
我們可以改進一下,使用微任務來包裹 onFulfilled 、onRejected,常用的微任務有 process.nextTick、MutationObserver、postMessage 等,我們這個使用 postMessage 改寫一下:
// ...
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數(shù)的返回值,resolve 出去
// 注冊一個 message 事件
window.addEventListener('message', event => {
const { type, data } = event.data;
if (type === '__promise') {
try {
let x = onFulfilled(that.value);
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
}
});
// 立馬執(zhí)行
window.postMessage({
type: '__promise',
}, "http://localhost:3001");
}
// ...
實現(xiàn)方法很簡單,我們監(jiān)聽window 的 message 事件,并在之后立馬觸發(fā)一個 postMessage 事件,這個時候其實 then 中的回調(diào)函數(shù)已經(jīng)在微任務隊列中了,我們重新運行一下例子,可以看到輸出的順序變?yōu)榱?then -> setTimeout。
當然
Promise內(nèi)部實現(xiàn)肯定沒有這么簡單,筆者在這里只是提供一種思路,大家有興趣可以去研究一波。
規(guī)范 resolvePromise 函數(shù)(規(guī)范 2.3)
重復引用
重復引用,當
x和promise2是一樣的,那就需要報一個錯誤,重復應用。(規(guī)范 2.3.1)
<br />
<br /> 因為自己等待自己完成是永遠都不會有結(jié)果的。
const p1 = new MyPromise((resolved, rejected) => {
resolved('我 resolved 了');
});
const p2 = p1.then((res) => {
return p2;
});
x 的類型
大致分為一下這么幾條:
- 2.3.2:當
x是一個Promise,那么就等待x改變狀態(tài)之后,才算完成或者失?。ㄟ@個也屬于2.3.3,因為Promise其實也是一個thenable對象) - 2.3.3:當
x是一個對象 或者 函數(shù)的時候,即thenable對象,那就那x.then作為then - 2.3.4:當
x不是一個對象,或者函數(shù)的時候,直接將x作為參數(shù)resolve返回。
我們主要看一下 2.3.3 就行,因為 Prmise 也屬于 thenable 對象,那什么是 thenable 對象呢?
簡單來說就是具有 then方法的對象/函數(shù),所有的
Promise對象都是thenable對象,但并非所有的thenable對象并非是Promise對象。如下:let thenable = { then: function(resolve, reject) { resolve(100); } }
根據(jù) x 的類型進行處理:
如果
x不是thenable對象,直接調(diào)用Promise2的resolve,將x作為成功的結(jié)果;當
x是thenable對象,會調(diào)用x的then方法,成功后再去調(diào)用resolvePromise函數(shù),并將執(zhí)行結(jié)果y作為新的x傳入resolvePromise,直到這個x值不再是一個thenable對象為止;如果失敗則直接調(diào)用promise2的reject。
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
if (typeof then === 'function') {
then.call(x, (y) => {
resolvePromise(promise2, y, resolve, reject);
}, (err) => {
reject(err);
})
}
} else {
resolve(x);
}
只調(diào)用一次
規(guī)范(
Promise/A+ 2.3.3.3.3)規(guī)定如果同時調(diào)用resolvePromise和rejectPromise,或者對同一參數(shù)進行了多次調(diào)用,則第一個調(diào)用優(yōu)先,而所有其他調(diào)用均被忽略,確保只執(zhí)行一次改變狀態(tài)。
我們在外面定義了一個 called 占位符,為了獲得 then 函數(shù)有沒有執(zhí)行過相應的改變狀態(tài)的函數(shù),執(zhí)行過了之后,就不再去執(zhí)行了,主要就是為了滿足規(guī)范。
x 為 Promise 對象
如果 x 是 Promise 對象的話,其實當執(zhí)行了resolve 函數(shù) 之后,就不會再執(zhí)行 reject 函數(shù)了,是直接在當前這個 Promise 對象就結(jié)束掉了。
x 為 thenable 對象
當 x 是普通的 thenable 函數(shù)的時候,他就有可能同時執(zhí)行 resolve 和 reject 函數(shù),即可以同時執(zhí)行 promise2 的 resolve 函數(shù) 和 reject 函數(shù),但是其實 promise2 在狀態(tài)改變了之后,也不會再改變相應的值了。其實也沒有什么問題,如下代碼:
// thenable 對像
{
then: function(resolve, reject) {
setTimeout(() => {
resolve('我是thenable對像的 resolve');
reject('我是thenable對像的 reject')
})
}
}
完整的 resolvePromise
完整的 resolvePromise 函數(shù)如下:
const resolvePromise = (promise2, x, resolve, reject) => {
if(x === promise2){
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if(called)return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if(called)return;
called = true;
reject(err);
})
} else {
resolve(x);
}
} catch (e) {
if(called)return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
到這里就大功告成了,開不開心,興不興奮!
最后我們可以通過測試腳本跑一下我們的 MyPromise 是否符合規(guī)范。
測試
有專門的測試腳本(promises-aplus-tests)可以幫助我們測試所編寫的代碼是否符合 Promise/A+ 的規(guī)范。
但是貌似只能測試
js文件,所以筆者就將ts文件轉(zhuǎn)化為了js文件,進行測試
在代碼里面加上:
// 執(zhí)行測試用例需要用到的代碼
MyPromise.deferred = function() {
let defer = {};
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
}
需要提前安裝一下測試插件:
# 安裝測試腳本
npm i -g promises-aplus-tests
# 開始測試
promises-aplus-tests MyPromise.js
結(jié)果如下:
完美通過,接下去我們就可以看看 Promise 更多方法的實現(xiàn)了。
?
更多方法
實現(xiàn)上面的 Promise 之后,其實編寫其實例和靜態(tài)方法,相對來說就簡單了很多。
實例方法
Promise.prototype.catch
實現(xiàn)
其實這個方法就是 then 方法的語法糖,只需要給 then 傳遞 onRejected 參數(shù)就 ok 了。
private catch(onRejected) {
return this.then(null, onRejected);
}
例子:
const p1 = new MyPromise((resolved, rejected) => {
resolved('resolved');
})
p1.then((res) => {
return new MyPromise((resolved, rejected) => {
setTimeout(() => {
rejected('錯誤了');
}, 1000)
});
})
.then((res) => {
return new MyPromise((resolved, rejected) => {
setTimeout(() => {
resolved('then2');
}, 1000)
});
})
.then((res) => {
return 'then3';
}).catch(error => {
console.log('----error', error);
})
// 1s 之后輸出:----error 錯誤了
Promise.prototype.finally
實現(xiàn)
finally() 方法用于指定不管 Promise 對象最后狀態(tài)如何,都會執(zhí)行的操作。
private finally (fn) {
return this.then(fn, fn);
}
例子
const p1 = new MyPromise((resolved, rejected) => {
resolved('resolved');
})
p1.then((res) => {
return new MyPromise((resolved, rejected) => {
setTimeout(() => {
rejected('錯誤了');
}, 1000)
});
})
.then((res) => {
return new MyPromise((resolved, rejected) => {
setTimeout(() => {
resolved('then2');
}, 1000)
});
})
.then((res) => {
return 'then3';
}).catch(error => {
console.log('---error', error);
return `catch-${error}`
}).finally(res => {
console.log('---finally---', res);
})
// 輸出結(jié)果:---error 錯誤了" -> ""---finally--- catch-錯誤了
?
靜態(tài)方法
Promise.resolve
實現(xiàn)
有時需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象,Promise.resolve()方法就起到這個作用。
static resolve = (val) => {
return new MyPromise((resolve,reject) => {
resolve(val);
});
}
例子
MyPromise.resolve({name: 'darrell', sex: 'boy' }).then((res) => {
console.log(res);
}).catch((error) => {
console.log(error);
});
// 輸出結(jié)果:{name: "darrell", sex: "boy"}
Promise.reject
實現(xiàn)
Promise.reject(reason) 方法也會返回一個新的 Promise 實例,該實例的狀態(tài)為 rejected。
static reject = (val) => {
return new MyPromise((resolve,reject) => {
reject(val)
});
}
例子
MyPromise.reject("出錯了").then((res) => {
console.log(res);
}).catch((error) => {
console.log(error);
});
// 輸出結(jié)果:出錯了
Promise.all
Promise.all()方法用于將多個 Promise 實例,包裝成一個新的 Promise 實例,
const p = Promise.all([p1, p2, p3]);
- 只有
p1、p2、p3的狀態(tài)都變成fulfilled,p的狀態(tài)才會變成fulfilled; - 只要
p1、p2、p3之中有一個被rejected,p的狀態(tài)就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調(diào)函數(shù)。
實現(xiàn)
static all = (promises: MyPromise[]) => {
return new MyPromise((resolve, reject) => {
let result: MyPromise[] = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].then(data => {
result[i] = data;
if (++count == promises.length) {
resolve(result);
}
}, error => {
reject(error);
});
}
});
}
例子
let Promise1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Promise1');
}, 2000);
});
let Promise2 = new MyPromise((resolve, reject) => {
resolve('Promise2');
});
let Promise3 = new MyPromise((resolve, reject) => {
resolve('Promise3');
})
let Promise4 = new MyPromise((resolve, reject) => {
reject('Promise4');
})
let p = MyPromise.all([Promise1, Promise2, Promise3, Promise4]);
p.then((res) => {
// 三個都成功則成功
console.log('---成功了', res);
}).catch((error) => {
// 只要有失敗,則失敗
console.log('---失敗了', err);
});
// 直接輸出:---失敗了 Promise4
Promise.race
Promise.race()方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
只要 p1、p2、p3 之中有一個實例率先改變狀態(tài),p 的狀態(tài)就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給 p 的回調(diào)函數(shù)。
實現(xiàn)
static race = (promises) => {
return new Promise((resolve,reject)=>{
for(let i = 0; i < promises.length; i++){
promises[i].then(resolve,reject)
};
})
}
例子
例子和 all 一樣,調(diào)用如下:
// ...
let p = MyPromise.race([Promise1, Promise2, Promise3, Promise4])
p.then((res) => {
console.log('---成功了', res);
}).catch((error) => {
console.log('---失敗了', err);
});
// 直接輸出:---成功了 Promise2
Promise.allSettled
此方法接受一組 Promise 實例作為參數(shù),包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
只有等到所有這些參數(shù)實例都返回結(jié)果,不管是 fulfilled 還是 rejected,而且該方法的狀態(tài)只可能變成 fulfilled。
此方法與
Promise.all的區(qū)別是all無法確定所有請求都結(jié)束,因為在all中,如果有一個被Promise被rejected,p的狀態(tài)就立馬變成rejected,有可能有些異步請求還沒走完。
實現(xiàn)
static allSettled = (promises: MyPromise[]) => {
return new MyPromise((resolve) => {
let result: MyPromise[] = [];
let count = 0;
for (let i = 0; i < promises.length; i++) {
promises[i].finally(res => {
result[i] = res;
if (++count == promises.length) {
resolve(result);
}
})
}
});
}
例子
例子和 all 一樣,調(diào)用如下:
let p = MyPromise.allSettled([Promise1, Promise2, Promise3, Promise4])
p.then((res) => {
// 三個都成功則成功
console.log('---成功了', res);
}, err => {
// 只要有失敗,則失敗
console.log('---失敗了', err);
})
// 2s 后輸出:---成功了 (4) ["Promise1", "Promise2", "Promise3", "Promise4"]
?
總結(jié)
這篇文章筆者帶大家一步一步的實現(xiàn)了符合 Promise/A+ 規(guī)范的的 Promise,看完之后相信大家基本上也能夠自己獨立寫出一個 Promise 來了。
最后通過幾個問題,大家可以看看自己掌握的如何:
-
Promise中是如何實現(xiàn)回調(diào)函數(shù)返回值穿透的? -
Promise出錯后,是怎么通過 冒泡 傳遞給最后那個捕獲異常的函數(shù)? -
Promise如何支持鏈式調(diào)用? - 怎么將
Promise.then包裝成一個微任務?
實不相瞞,想要個贊!
?
參考文檔
- 阮一峰 ES6 Promise教程
- promise 迷你書
- Promises/A+ 規(guī)范文檔
- 剖析Promise內(nèi)部結(jié)構(gòu),一步一步實現(xiàn)一個完整的、能通過所有Test case的Promise類
- 30分鐘,讓你徹底明白Promise原理
- 手動實現(xiàn)一個滿足promises-aplus-tests的Promise
- 面試官:請用一句話描述 try catch 能捕獲到哪些 JS 異常
?
示例代碼
示例代碼可以看這里: