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