關於 Promise
原理解析的優秀文章,在掘金上已經有很是多了。可是筆者老是處在 看了就會,一寫就廢 的狀態,這是筆者寫這篇文章的目的,爲了理一下 Promise
的編寫思路,從零開始手寫一波代碼,同時也方便本身往後回顧。javascript
Promise
是 JavaScript
異步編程的一種流行解決方案,它的出現是爲了解決 回調地獄 的問題,讓使用者能夠經過鏈式的寫法去編寫寫異步代碼,具體的用法筆者就不介紹了,你們能夠參考阮一峯老師的 ES6 Promise教程。java
什麼是觀察者模式:git
觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個目標對象,當這個目標對象的狀態發生變化時,會通知全部觀察者對象,使它們可以自動更新。es6
Promise
是基於 觀察者的設計模式 實現的,then
函數要執行的函數會被塞入觀察者數組中,當 Promise
狀態變化的時候,就去執行觀察組數組中的全部函數。github
實現 Promise
涉及到了 JavaScript
中的事件循環機制 EventLoop
、以及宏任務和微任務的概念。面試
事件循環機制的流程圖以下:typescript
你們能夠看一下這段代碼:shell
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);
複製代碼
若是不能一會兒說出輸出結果,建議你們能夠先查閱一下 事件循環 的相關資料,在掘金中有不少優秀的文章。npm
Promises/A+ 是一個社區規範,若是你想寫出一個規範的 Promise
,咱們就須要遵循這個標準。以後咱們也會根據規範來完善咱們本身編寫的 Promise
。編程
在動手寫 Promise
以前,咱們先過一下幾個重要的知識點。
// 建立 Promise 對象 x1
// 並在 executor 函數中執行業務邏輯
function executor(resolve, reject){
// 業務邏輯處理成功結果
const value = ...;
resolve(value);
// 失敗結果
// const reason = ...;
// reject(reason);
}
let x1 = new Promise(executor);
複製代碼
首先 Promise
是一個類,它接收一個執行函數 executor
,它接收兩個參數:resolve
和 reject
,這兩個參數是 Promise
內部定義的兩個函數,用來改變狀態並執行對應回調函數。
由於
Promise
自己是不知道執行結果失敗或者成功,它只是給異步操做提供了一個容器,實際上的控制權在使用者的手上,使用者能夠調用上面兩個參數告訴Promise
結果是否成功,同時將業務邏輯處理結果(value/reason
)做爲參數傳給resolve
和reject
兩個函數,執行回調。
Promise
有三個狀態:
pending
:等待中resolved
:已成功rejected
:已失敗在 Promise
的狀態改變只有兩種可能:從 pending
變爲 resolved
或者從 pending
變爲 rejected
,以下圖(引自 Promise 迷你書):
並且須要注意的是一旦狀態改變,狀態不會再變了,接下來就一直是這個結果。也就是說當咱們在 executor
函數中調用了 resolve
以後,以後調用 reject
就沒有效果了,反之亦然。
// 並在 executor 函數中執行業務邏輯
function executor(resolve, reject){
resolve(100);
// 以後調用 resolve,reject 都是無效的,
// 由於狀態已經變爲 resolved,不會再改變了
reject(100);
}
let x1 = new Promise(executor);
複製代碼
每個 promise
都一個 then
方法,這個是當 promise
返回結果以後,須要執行的回調函數,他有兩個可選參數:
onFulfilled
:成功的回調;onRejected
:失敗的回調;以下圖(引自 Promise 迷你書):
// ...
let x1 = new Promise(executor);
// x1 延遲綁定回調函數 onResolve
function onResolved(value){
console.log(value);
}
// x1 延遲綁定回調函數 onRejected
function onRejected(reason){
console.log(reason);
}
x1.then(onResolved, onRejected);
複製代碼
在這裏咱們簡單過一下手寫一個 Promise
的大體流程:
new Promise
時,須要傳遞一個 executor
執行器函數,在構造函數中,執行器函數馬上執行executor
執行函數接受兩個參數,分別是 resolve
和 reject
Promise
只能從 pending
到 rejected
, 或者從 pending
到 fulfilled
Promise
的狀態一旦確認,狀態就凝固了,不在改變Promise
都有 then
方法,then
接收兩個參數,分別是 Promise
成功的回調 onFulfilled
,和失敗的回調 onRejected
then
時,Promise
已經成功,則執行 onFulfilled
,並將 Promise
的值做爲參數傳遞進去;若是 Promise
已經失敗,那麼執行 onRejected
,並將 Promise
失敗的緣由做爲參數傳遞進去;若是 Promise
的狀態是 pending
,須要將 onFulfilled
和 onRejected
函數存放起來,等待狀態肯定後,再依次將對應的函數執行(觀察者模式)then
的參數 onFulfilled
和 onRejected
能夠不傳,Promise
能夠進行值穿透。Promise
能夠 then
屢次,Promise
的 then
方法返回一個新的 Promise
。then
返回的是一個正常值,那麼就會把這個結果(value
)做爲參數,傳遞給下一個 then
的成功的回調(onFulfilled
)then
中拋出了異常,那麼就會把這個異常(reason
)做爲參數,傳遞給下一個 then
的失敗的回調(onRejected
)then
返回的是一個 promise
或者其餘 thenable
對象,那麼須要等這個 promise
執行完撐,promise
若是成功,就走下一個 then
的成功回調;若是失敗,就走下一個 then
的失敗回調。上面是大體的實現流程,若是迷迷糊糊不要緊,只要大體有一個印象便可,後續咱們會一一講到。
那接下來咱們就開始實現一個最簡單的例子開始講解。
咱們先寫一個簡單版,這版暫不支持狀態、鏈式調用,而且只支持調用一個
then
方法。
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
p1.then((data) => {
console.log(data);
}, (err) => {
console.log(err);
})
複製代碼
例子很簡單,就是 1s
以後返回 成功了
,並在 then
中輸出。
咱們定義一個 MyPromise
類,接着咱們在其中編寫代碼,具體代碼以下:
class MyPromise {
// ts 接口定義 ...
constructor (executor: executor) {
// 用於保存 resolve 的值
this.value = null;
// 用於保存 reject 的值
this.reason = null;
// 用於保存 then 的成功回調
this.onFulfilled = null;
// 用於保存 then 的失敗回調
this.onRejected = null;
// executor 的 resolve 參數
// 用於改變狀態 並執行 then 中的成功回調
let resolve = value => {
this.value = value;
this.onFulfilled && this.onFulfilled(this.value);
}
// executor 的 reject 參數
// 用於改變狀態 並執行 then 中的失敗回調
let reject = reason => {
this.reason = reason;
this.onRejected && this.onRejected(this.reason);
}
// 執行 executor 函數
// 將咱們上面定義的兩個函數做爲參數 傳入
// 有可能在 執行 executor 函數的時候會出錯,因此須要 try catch 一下
try {
executor(resolve, reject);
} catch(err) {
reject(err);
}
}
// 定義 then 函數
// 而且將 then 中的參數複製給 this.onFulfilled 和 this.onRejected
private then(onFulfilled, onRejected) {
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
}
複製代碼
好了,咱們的初版就完成了,是否是很簡單。
不過這裏須要注意的是,
resolve
函數的執行時機須要在then
方法將回調函數註冊了以後,在resolve
以後在去往賦值回調函數,其實已經完了,沒有任何意義。上面的例子沒有問題,是由於
resolve(成功了)
是包在setTimeout
中的,他會在下一個宏任務執行,這時回調函數已經註冊了。你們能夠試試把
resolve(成功了)
從setTimeout
中拿出來,這個時候就會出現問題了。
這一版實現很簡單,還存在幾個問題:
未引入狀態的概念,如今狀態能夠隨意變,不符合 Promise
狀態只能從等待態變化的規則。
正常狀況下咱們能夠對 Promise
進行鏈式調用:
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
p1.then(onResolved1, onRejected1).then(onResolved2, onRejected2)
複製代碼
在這個例子中,onResolved2
會覆蓋 onResolved1
,onRejected2
會覆蓋 onRejected1
。
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
// 註冊多個回調函數
p1.then(onResolved1, onRejected1);
p1.then(onResolved2, onRejected2);
複製代碼
接下來咱們更進一步,把這些問題給解決掉。
這一版咱們把狀態的概念引入,同時實現鏈式調用的功能。
上面咱們說到 Promise
有三個狀態:pending
、resovled
、rejected
,只能從 pending
轉爲 resovled
或者 rejected
,並且當狀態改變以後,狀態就不能再改變了。
status
:用於記錄當前 Promise
的狀態PENDING
、RESOLVED
、REJECTED
。then
的成功回調定義爲一個數組:this.resolvedQueues
與 this.rejectedQueues
,咱們能夠把 then
中的回調函數都塞入對應的數組中,這樣就能解決咱們上面提到的第三個問題。class MyPromise {
private static PENDING = 'pending';
private static RESOLVED = 'resolved';
private static REJECTED = 'rejected';
constructor (executor: executor) {
this.status = MyPromise.PENDING;
// ...
// 用於保存 then 的成功回調數組
this.resolvedQueues = [];
// 用於保存 then 的失敗回調數組
this.rejectedQueues = [];
let resolve = value => {
// 當狀態是 pending 是,將 promise 的狀態改成成功態
// 同時遍歷執行 成功回調數組中的函數,將 value 傳入
if (this.status == MyPromise.PENDING) {
this.value = value;
this.status = MyPromise.RESOLVED;
this.resolvedQueues.forEach(cb => cb(this.value))
}
}
let reject = reason => {
// 當狀態是 pending 是,將 promise 的狀態改成失敗態
// 同時遍歷執行 失敗回調數組中的函數,將 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
中的方法,以前咱們是直接將 then
的兩個參數 onFulfilled
和 onRejected
,直接賦值給了 Promise
的用於保存成功、失敗函數回調的實例屬性。
如今咱們須要將這兩個屬性塞入到兩個數組中去:resolvedQueues
和 rejectedQueues
。
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
// 首先判斷兩個參數是否爲函數類型,由於這兩個參數是可選參數
// 當參數不是函數類型時,須要建立一個函數賦值給對應的參數
// 這也就實現了 透傳
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}
// 當狀態是等待態的時候,須要將兩個參數塞入到對應的回調數組中
// 當狀態改變以後,在執行回調函數中的函數
if (this.status === MyPromise.PENDING) {
this.resolvedQueues.push(onFulfilled)
this.rejectedQueues.push(onRejected)
}
// 狀態是成功態,直接就調用 onFulfilled 函數
if (this.status === MyPromise.RESOLVED) {
onFulfilled(this.value)
}
// 狀態是成功態,直接就調用 onRejected 函數
if (this.status === MyPromise.REJECTED) {
onRejected(this.reason)
}
}
}
複製代碼
this.status
會是 pending
狀態,什麼狀況下會是 resolved
狀態這個其實也和事件循環機制有關,以下代碼:
// this.status 爲 pending 狀態
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
}).then(value => {
console.log(value)
})
// this.status 爲 resolved 狀態
new MyPromise((resolve, reject) => {
resolve(1)
}).then(value => {
console.log(value)
})
複製代碼
以下面代碼,當 then
中沒有傳任何參數的時候,Promise
會使用內部默認的定義的方法,將結果傳遞給下一個 then
。
let p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolved('成功了');
}, 1000);
})
p1.then().then((res) => {
console.log(res);
})
複製代碼
由於咱們如今還沒支持鏈式調用,這段代碼運行會出問題。
支持鏈式調用,其實很簡單,咱們只須要給 then
函數最後返回 this
就行,這樣就支持了鏈式調用:
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
// ...
return this;
}
}
複製代碼
每次調用 then
以後,咱們都返回當前的這個 Promise
對象,由於 Promise
對象上是存在 then
方法的,這個時候咱們就簡單的實現了 Promise
的簡單調用。
這個時候運行上面 透傳 的測試代碼了。
可是上面的代碼仍是存在相應的問題的,看下面代碼:
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
以後,其實狀態已經從 pending
態變爲了 resolved
態,以後不會再變了,因此在 MyPromise
中的 this.value
值就一直是 resolved
。
這個時候咱們就得看看關於 then
返回值的相關知識點了。
實際上 then
都會返回了一個新的 Promise
對象。
先看下面這段代碼:
// 新建立一個 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
已經再也不是最初的 Promise
了,以下圖(引自 Promise 迷你書):
promise
的鏈式調用跟jQuery
的鏈式調用是有區別的,jQuery
鏈式調用返回的對象仍是最初那個jQuery
對象;Promise
更相似於數組中一些方法,如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) => {
// 成功狀態,直接 resolve
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數的返回值,resolve 出去
let x = onFulfilled(this.value);
resolve(x);
}
// 失敗狀態,直接 reject
if (this.status === MyPromise.REJECTED) {
// 將 onRejected 函數的返回值,reject 出去
let x = onRejected(this.reason)
reject && reject(x);
}
// 等待狀態,將 onFulfilled,onRejected 塞入數組中,等待回調執行
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;
}
}
// 輸出結果 resolved -> then1 -> then2
複製代碼
到這裏咱們就完成了簡單的鏈式調用,可是隻能支持同步的鏈式調用,若是咱們須要在 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
對象直接看成參數傳給下一個 then
函數,而咱們實際上是想要將這個 Promise
的處理結果傳遞下去。
這一版咱們來實現
promise
的異步鏈式調用。
先看一下 then
中 onFulfilled
和 onRejected
返回的值:
// 成功的函數返回
let x = onFulfilled(this.value);
// 失敗的函數返回
let x = onRejected(this.reason);
複製代碼
從上面的的問題中能夠看出,x
能夠是一個 普通值,也能夠是一個 Promise
對象,普通值的傳遞咱們在 第二版 已經解決了,如今須要解決的是當 x
返回一個 Promise
對象的時候該怎麼處理。
其實也很簡單,當 x
是一個 Promise
對象的時候,咱們須要進行等待,直到返回的 Promise
狀態變化的時候,再去執行以後的 then
函數,代碼以下:
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) => {
// 成功狀態,直接 resolve
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數的返回值,resolve 出去
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}
// 失敗狀態,直接 reject
if (this.status === MyPromise.REJECTED) {
// 將 onRejected 函數的返回值,reject 出去
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject);
}
// 等待狀態,將 onFulfilled,onRejected 塞入數組中,等待回調執行
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;
}
}
複製代碼
咱們新寫一個函數 resolvePromise
,這個函數是用來處理異步鏈式調用的核心方法,他會去判斷 x
返回值是否是 Promise
對象,若是是的話,就直到 Promise
返回成功以後在再改變狀態,若是是普通值的話,就直接將這個值 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
接受四個參數:
promise2
是 then
中返回的 promise
;x
是 then
的兩個參數 onFulfilled
或者 onRejected
的返回值,類型不肯定,有多是普通值,有多是 thenable
對象;resolve
和 reject
是 promise2
的。當 x
是 Promise
的時,而且他的狀態是 Pending
狀態,若是 x
執行成功,那麼就去遞歸調用 resolvePromise
這個函數,將 x
執行結果做爲 resolvePromise
第二個參數傳入;
若是執行失敗,則直接調用 promise2
的 reject
方法。
到這裏咱們基本上一個完整的 promise
,接下來咱們須要根據 Promises/A+ 來規範一下咱們的 Promise
。
前幾版的代碼筆者基本上是按照規範來的,這裏主要講幾個沒有符合規範的點。
then
中onFulfilled
和onRejected
須要異步執行,即放到異步任務中去執行(規範 2.2.4)
咱們須要將 then
中的函數經過 setTimeout
包裹起來,放到一個宏任務中去,這裏涉及了 js
的 EventLoop
,你們能夠去看看相應的文章,以下:
class MyPromise {
// ...
private then(onFulfilled, onRejected) {
// ...
// then 方法返回一個新的 promise
const promise2 = new MyPromise((resolve, reject) => {
// 成功狀態,直接 resolve
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數的返回值,resolve 出去
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
})
}
// 失敗狀態,直接 reject
if (this.status === MyPromise.REJECTED) {
// 將 onRejected 函數的返回值,reject 出去
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
})
}
// 等待狀態,將 onFulfilled,onRejected 塞入數組中,等待回調執行
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
是屬於微任務的,如今當使用 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
複製代碼
輸出順序不同,緣由是由於如今的 Promise
是經過 setTimeout
宏任務包裹的。
咱們能夠改進一下,使用微任務來包裹 onFulfilled
、onRejected
,經常使用的微任務有 process.nextTick
、MutationObserver
、postMessage
等,咱們這個使用 postMessage
改寫一下:
// ...
if (this.status === MyPromise.RESOLVED) {
// 將 onFulfilled 函數的返回值,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);
}
}
});
// 立馬執行
window.postMessage({
type: '__promise',
}, "http://localhost:3001");
}
// ...
複製代碼
實現方法很簡單,咱們監聽window
的 message
事件,並在以後立馬觸發一個 postMessage
事件,這個時候其實 then
中的回調函數已經在微任務隊列中了,咱們從新運行一下例子,能夠看到輸出的順序變爲了 then -> setTimeout
。
固然
Promise
內部實現確定沒有這麼簡單,筆者在這裏只是提供一種思路,你們有興趣能夠去研究一波。
重複引用,當
x
和promise2
是同樣的,那就須要報一個錯誤,重複應用。(規範 2.3.1)
由於本身等待本身完成是永遠都不會有結果的。
const p1 = new MyPromise((resolved, rejected) => {
resolved('我 resolved 了');
});
const p2 = p1.then((res) => {
return p2;
});
複製代碼
大體分爲一下這麼幾條:
x
是一個 Promise
,那麼就等待 x
改變狀態以後,纔算完成或者失敗(這個也屬於 2.3.3
,由於 Promise
其實也是一個 thenable
對象)x
是一個對象 或者 函數的時候,即 thenable
對象,那就那 x.then
做爲 then
x
不是一個對象,或者函數的時候,直接將 x
做爲參數 resolve
返回。咱們主要看一下 2.3.3
就行,由於 Prmise
也屬於 thenable
對象,那什麼是 thenable
對象呢?
簡單來講就是具備 then方法的對象/函數,全部的
Promise
對象都是thenable
對象,但並不是全部的thenable
對象並不是是Promise
對象。以下:let thenable = { then: function(resolve, reject) { resolve(100); } } 複製代碼
根據 x
的類型進行處理:
若是 x
不是 thenable
對象,直接調用 Promise2
的 resolve
,將 x
做爲成功的結果;
當 x
是 thenable
對象,會調用 x
的 then
方法,成功後再去調用 resolvePromise
函數,並將執行結果 y
做爲新的 x
傳入 resolvePromise
,直到這個 x
值再也不是一個 thenable
對象爲止;若是失敗則直接調用 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);
}
複製代碼
規範(
Promise/A+ 2.3.3.3.3
)規定若是同時調用resolvePromise
和rejectPromise
,或者對同一參數進行了屢次調用,則第一個調用優先,而全部其餘調用均被忽略,確保只執行一次改變狀態。
咱們在外面定義了一個 called
佔位符,爲了得到 then
函數有沒有執行過相應的改變狀態的函數,執行過了以後,就再也不去執行了,主要就是爲了知足規範。
若是 x
是 Promise
對象的話,其實當執行了resolve
函數 以後,就不會再執行 reject
函數了,是直接在當前這個 Promise
對象就結束掉了。
當 x
是普通的 thenable
函數的時候,他就有可能同時執行 resolve
和 reject
函數,便可以同時執行 promise2
的 resolve
函數 和 reject
函數,可是其實 promise2
在狀態改變了以後,也不會再改變相應的值了。其實也沒有什麼問題,以下代碼:
// thenable 對像
{
then: function(resolve, reject) {
setTimeout(() => {
resolve('我是thenable對像的 resolve');
reject('我是thenable對像的 reject')
})
}
}
複製代碼
完整的 resolvePromise
函數以下:
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
是否符合規範。
有專門的測試腳本(promises-aplus-tests
)能夠幫助咱們測試所編寫的代碼是否符合 Promise/A+
的規範。
可是貌似只能測試
js
文件,因此筆者就將ts
文件轉化爲了js
文件,進行測試
在代碼裏面加上:
// 執行測試用例須要用到的代碼
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
複製代碼
結果以下:
完美經過,接下去咱們就能夠看看 Promise
更多方法的實現了。
實現上面的 Promise
以後,其實編寫其實例和靜態方法,相對來講就簡單了不少。
其實這個方法就是 then
方法的語法糖,只須要給 then
傳遞 onRejected
參數就 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 錯誤了
複製代碼
finally()
方法用於指定無論 Promise
對象最後狀態如何,都會執行的操做。
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);
})
// 輸出結果:---error 錯誤了" -> ""---finally--- catch-錯誤了
複製代碼
有時須要將現有對象轉爲 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);
});
// 輸出結果:{name: "darrell", sex: "boy"}
複製代碼
Promise.reject(reason)
方法也會返回一個新的 Promise
實例,該實例的狀態爲 rejected
。
static reject = (val) => {
return new MyPromise((resolve,reject) => {
reject(val)
});
}
複製代碼
MyPromise.reject("出錯了").then((res) => {
console.log(res);
}).catch((error) => {
console.log(error);
});
// 輸出結果:出錯了
複製代碼
Promise.all()
方法用於將多個 Promise
實例,包裝成一個新的 Promise
實例,
const p = Promise.all([p1, p2, p3]);
複製代碼
p1
、p2
、p3
的狀態都變成 fulfilled
,p
的狀態纔會變成 fulfilled
;p1
、p2
、p3
之中有一個被 rejected
,p
的狀態就變成 rejected
,此時第一個被 reject
的實例的返回值,會傳遞給p
的回調函數。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 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
複製代碼
只要 p1
、p2
、p3
之中有一個實例率先改變狀態,p
的狀態就跟着改變。那個率先改變的 Promise
實例的返回值,就傳遞給 p
的回調函數。
static race = (promises) => {
return new Promise((resolve,reject)=>{
for(let i = 0; i < promises.length; i++){
promises[i].then(resolve,reject)
};
})
}
複製代碼
例子和 all
同樣,調用以下:
// ...
let p = MyPromise.race([Promise1, Promise2, Promise3, Promise4])
p.then((res) => {
console.log('---成功了', res);
}).catch((error) => {
console.log('---失敗了', err);
});
// 直接輸出:---成功了 Promise2
複製代碼
此方法接受一組 Promise
實例做爲參數,包裝成一個新的 Promise
實例。
const p = Promise.race([p1, p2, p3]);
複製代碼
只有等到全部這些參數實例都返回結果,不論是 fulfilled
仍是 rejected
,並且該方法的狀態只可能變成 fulfilled
。
此方法與
Promise.all
的區別是all
沒法肯定全部請求都結束,由於在all
中,若是有一個被Promise
被rejected
,p
的狀態就立馬變成rejected
,有可能有些異步請求還沒走完。
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
同樣,調用以下:
let p = MyPromise.allSettled([Promise1, Promise2, Promise3, Promise4])
p.then((res) => {
// 三個都成功則成功
console.log('---成功了', res);
}, err => {
// 只要有失敗,則失敗
console.log('---失敗了', err);
})
// 2s 後輸出:---成功了 (4) ["Promise1", "Promise2", "Promise3", "Promise4"]
複製代碼
這篇文章筆者帶你們一步一步的實現了符合 Promise/A+
規範的的 Promise
,看完以後相信你們基本上也可以本身獨立寫出一個 Promise
來了。
最後經過幾個問題,你們能夠看看本身掌握的如何:
Promise
中是如何實現回調函數返回值穿透的?Promise
出錯後,是怎麼經過 冒泡 傳遞給最後那個捕獲異常的函數?Promise
如何支持鏈式調用?Promise.then
包裝成一個微任務?實不相瞞,想要個贊!
示例代碼能夠看這裏: