Promise進階——如何實現一個Promise庫

黃Java發表於2019-01-02

概述

從上次更新Promise/A+規範後,已經很久沒有更新部落格了。之前由於業務需要,完成了一個TypeScript語言的Promise庫。這次我們來和大家一步一步介紹下,我們如何實現一個符合Promise/A+規範的Promise庫。

如果對Promise/A+規範還不太瞭解的同學,建議先看看上一篇部落格—— 
[譯]前端基礎知識儲備——Promise/A+規範 

實現流程

首先,我們來看下,在我實現的這一個Promise中,程式碼由下面這幾部分組成:

  • 全域性非同步函式執行器
  • 常量與屬性
  • 類方法
  • 類靜態方法

通過上面這四個部分,我們就能夠得到一個完整的Promise。這四個部分互相有關聯,接下來我們一個一個模組來看。

全域性非同步函式執行器

在之前的Promiz的原始碼分析的部落格中我有提到過,我們如何來實現一個非同步函式執行器。通過JavaScript的執行原理我們可以知道,如果要實現非同步執行相關函式的話,我們可以選擇使用巨集任務和微任務,這一點在Promise/A+的規範中也有提及。因此,下面我們提供了一個用巨集任務來實現非同步函式執行器的程式碼供大家參考。

let index = 0;
if (global.postMessage) {
global.addEventListener('message', (e) =>
{
if (e.source === global) {
let id = e.data;
if (isRunningTask) {
nextTick(functionStorage[id]);

} else {
isRunningTask = true;
try {
functionStorage[id]();

} catch (e) {

} isRunningTask = false;

} delete functionStorage[id];
functionStorage[id] = void 0;

}
});

}function nextTick(func) {
if (global.setImmediate) {
global.setImmediate(func);

} else if (global.postMessage) {
functionStorage[++index] = func;
global.postMessage(index, '*')
} else {
setTimeout(func);

}
}複製程式碼

通過上面的程式碼我們可以看到,我們一共使用了setImmediatepostMessagesetTimeout這三個新增巨集任務的方法來進行一步函式執行。

常量與屬性

說完了如何進行非同步函式的執行,我們來看下相關的常量與屬性。在實現Promise之前,我們需要定義一些常量和類屬性,用於後面儲存資料。讓我們一個一個來看下。

常量

首先,Promise共有5個狀態,我們需要用常量來進行定義,具體如下:

enum State { 
pending = 0, resolving = 1, rejecting = 2, resolved = 3, rejected = 4
};
複製程式碼

這五個常量分別對應Promise中的5個狀態,相信大家能夠從名字中理解,我們就不多講了。

屬性

在Promise中,我們需要一些屬性來儲存資料狀態和後續的Promise引用,具體如下:

class Promise { 
private _value;
private _reason;
private _next = [];
public state: State = 0;
public fn;
public er;

}複製程式碼

我們對屬性進行逐一說明:

  • _value,表示在resolved狀態時,用來儲存當前的值。
  • _reason,表示在rejected狀態時,用來儲存當前的原因。
  • _next,表示當前Promise後面跟著then函式的引用。
  • fn,表示當前Promise中的then方法的第一個回撥函式。
  • er,表示當前Promise中的then方法的的第二個回撥函式(即catch的第一個引數,下面看catch實現方法就能理解)。

類方法

看完了常量與類的屬性,我們來看下類的靜態方法。

Constructor

首先,如果我們要實現一個Promise,我們需要一個建構函式來初始化最初的Promise。具體程式碼如下:

class Promise { 
constructor(resolver?) {
if (typeof resolver !== 'function' &
&
resolver !== undefined) {
throw TypeError()
} if (typeof this !== 'object') {
throw TypeError()
} try {
if (typeof resolver === 'function') {
resolver(this.resolve.bind(this), this.reject.bind(this));

}
} catch (e) {
this.reject(e);

}
}
}複製程式碼

從Promise/A+的規範來看,我們可以知道,如果resolver存在並且不是一個function的話,那麼我們就應該丟擲一個錯誤;否則,我們應該將resolvereject方法傳給resolver作為引數。

resolve &
&
reject

那麼,resolvereject方法又是做什麼的呢?這兩個方法主要是用來讓當前的這個Promise轉換狀態的,即從pending狀態轉換為resolving或者rejecting狀態。下面讓我們來具體看下程式碼:

class Promise { 
resolve(value) {
if (this.state === State.pending) {
this._value = value;
this.state = State.resolving;
nextTick(this._handleNextTick.bind(this));

} return this;

} reject(reason) {
if (this.state === State.pending) {
this._reason = reason;
this.state = State.rejecting;
this._value = void 0;
nextTick(this._handleNextTick.bind(this));

} return this;

}
}複製程式碼

從上面的程式碼中我們可以看到,當resolve或者reject方法被觸發時,我們都改變了當前這個Proimse的狀態,並且非同步呼叫執行了_handleNextTick方法。狀態的改變標誌著當前的Promise已經從pending狀態改變成了resolving或者rejecting狀態,而相應_value_reson也表示上一個Promise傳遞給下一個Promise的資料。

那麼,這個_handleNextTick方法又是做什麼的呢?其實,這個方法的作用很簡單,就是用來處理當前這個Promise後面跟著的then函式傳遞進來的回撥函式fner

then &
&
catch

在瞭解_handleNextTick之前,我們們先看下then函式和catch函式的實現。

class Promise { 
public then(fn, er?) {
let promise = new Promise();
promise.fn = fn;
promise.er = er;
if (this.state === State.resolved) {
promise.resolve(this._value);

} else if (this.state === State.rejected) {
promise.reject(this._reason);

} else {
this._next.push(promise);

} return promise;

} public catch(er) {
return this.then(null, er);

}
}複製程式碼

因為catch函式呼叫就是一個then函式的別名,我們下面就只討論then函式。

then函式執行時,我們會建立一個新的Promise,然後將傳入的兩個回撥函式用新的Promise的屬性儲存下來。然後,先判斷當前的Promise的狀態,如果已經是resolved或者rejected狀態時,我們立即呼叫新的Promise中resolve或者reject方法,讓下將當前Promise的_value或者_reason傳遞給下一個Promise,並且觸發下一個Promise的狀態改變。如果當前Promise的狀態仍然為pending時,那麼就將這個新生成的Promise儲存下來,等當前這個Promise的狀態改變後,再觸發新的Promise變化。最後,我們返回了這個Promise的例項。

handleNextTick

看完了then函式,我們就可以來看下我們提到過的handleNextTick函式。

class Promise { 
private _handleNextTick() {
try {
if (this.state === State.resolving &
&
typeof this.fn === 'function') {
this._value = this.fn.call(getThis(), this._value);

} else if (this.state === State.rejecting &
&
typeof this.er === 'function') {
this._value = this.er.call(getThis(), this._reason);
this.state = 1;

}
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();

} // if promise === x, use TypeError to reject promise // 如果promise和x指向同一個物件,那麼用TypeError作為原因拒絕promise if (this._value === this) {
this.state = State.rejecting;
this._reason = new TypeError();
this._value = void 0;

} this._finishThisTypeScriptPromise();

}
}複製程式碼

我們先來看一個簡單版的_handleNextTick函式,這樣能夠幫助我們快速理解Promise主流程。

非同步觸發了_handleNextTick函式後,我們會判斷當前使用者處於的狀態,如果當前Promise是resolving狀態,我們就會呼叫fn函式,即我們在then函式呼叫時給新的Promise設定的那個fn函式;而如過當前Promise是rejecting狀態,我們就會呼叫er函式。

上面提到的getThis方法是用來獲取特定的this值,具體的規範要求我們將在稍後再進行介紹。

通過執行這兩個同步的fner函式,我們能夠得到當前Promise執行完傳入回撥後的值。在這裡需要說明的是:我們在執行fn或者er函式之前,我們在_value_reason中存放的值,是上一個Promise傳遞下來的值。只有當執行完了fn或者er函式後,_value_reason中存放的值才是我們需要傳遞給下一個Promise的值。

大家到這裡可能會奇怪,我們的this指向沒有發生變化,但是為什麼我們的this指向的是那個新的Promise,而不是原來的那個Promise呢?

我們可以從另外一個角度來看待這個問題:我們當前的這個Promise是不是由上一個Promise所產生的呢?如果是這種情況的話,我們就可以理解,在上一個Promise產生當前Promise的時候,就設定了fner兩個函式。

大家可能又會問,那麼我們第一個Promise的fner這兩個引數是怎麼來的呢?

那麼我們就需要仔細看下上面這個邏輯了。下面我們只討論第一個Promise處於pending的情況,其餘的情況與這種情形基本相同。第一個Promise因為沒有上一個Promise去設定fner兩個引數,因此這兩個引數的值就是undefined。所以在上面的邏輯中,我們已經排除了這種情況,直接進入了_finishThisTypeScriptPromise函式中。

我們在這裡需要特別說明下的是,有些人會認為我們在呼叫then函式傳入的兩個回撥函式fner時,當前Promise就結束了,其實並不是這樣,我們是得到了fn或者er兩個函式的返回值,再將值傳遞給下一個Promise時,上一個Promise才會結束。關於這個邏輯我們可以看下_finishThisTypeScriptPromise函式。

finishThisTypeScriptPromise

_finishThisTypeScriptPromise函式的程式碼如下:

class Promise { 
private _finishThisTypeScriptPromise() {
if (this.state === State.resolving) {
this.state = State.resolved;
this._next.map((nextTypeScriptPromise) =>
{
nextTypeScriptPromise.resolve(this._value);

});

} else {
this.state = State.rejected;
this._next.map((nextTypeScriptPromise) =>
{
nextTypeScriptPromise.reject(this._reason);

});

}
}
}複製程式碼

_finishThisTypeScriptPromise函式中我們可以看到,我們在得到了需要傳遞給下一個Promise的_value或者_reason後,利用map方法逐個呼叫我們儲存的新生成的Promise例項,呼叫它的resolve方法,因此我們又觸發了這個Promise的狀態從pending轉變為resolving或者rejecting

到這裡我們就已經完全瞭解了一個Promise從最開始的建立,到最後結束的整個生命週期。下面我們來看下在Promise/A+規範中提到的一些分支邏輯的處理情況。

上一個Promise傳遞的value是Thenable例項

首先,讓我們來了解下什麼是Thenable例項。Thenable例項指的是屬性中有then函式的物件。Promise就是的一種特殊的Thenable物件。

下面,為了方便講解,我們將用Promise來代替Thenable進行講解,其他的Thenable類大家可以參考類似思路進行分析。

如果我們在傳遞給我們的_value中是一個Promise例項,那麼我們必須在等待傳入的Promise狀態轉換到resolved之後,當前的Promise才能夠繼續往下執行,即我們從傳入的Promise中得到了一個非Thenable返回值時,我們才能用這個值來呼叫屬性中的fn或者er方法。

那麼,我們要怎麼樣才能獲取到傳入的這個Promise的返回值呢?在Promise中其實用到了一個非常巧妙的方法:因為傳入的Promise中有一個then函式(Thenable定義),因此我們就呼叫then函式,在第一個回撥函式fn中傳入獲取_value,觸發當前的Promise繼續執行。如果是觸發了第二個回撥函式er,那麼就用在er中得到的_reason來拒絕掉當前的Promise。具體判斷邏輯如下:

class Promise { 
private _handleNextTick() {
let ref;
let count = 0;
try {
// 判斷傳入的this._value是否為一個thanable // check if this._value a thenable ref = this._value &
&
this._value.then;

} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
return this._handleNextTick();

} if (this.state !== State.rejecting &
&
(typeof this._value === 'object' || typeof this._value === 'function') &
&
typeof ref === 'function') {
// add a then function to get the status of the promise // 在原有TypeScriptPromise後增加一個then函式用來判斷原有promise的狀態 try {
ref.call(this._value, (value) =>
{
if (count++) {
return;

} this._value = value;
this.state = State.resolving;
this._handleNextTick();

}, (reason) =>
{
if (count++) {
return;

} this._reason = reason;
this.state = State.rejecting;
this._value = void 0;
this._handleNextTick();

});

} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._handleNextTick();

}
} else {
try {
if (this.state === State.resolving &
&
typeof this.fn === 'function') {
this._value = this.fn.call(getThis(), this._value);

} else if (this.state === State.rejecting &
&
typeof this.er === 'function') {
this._value = this.er.call(getThis(), this._reason);
this.state = 1;

}
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();

} this._finishThisTypeScriptPromise();

}
}
}複製程式碼

promise === value

在Promise/A+規範中,如果返回的_value值等於使用者自身時,需要用TypeError錯誤拒絕掉當前的Promise。因此我們需要在_handleNextTick中加入以下判斷程式碼:

class Promise { 
private _handleNextTick() {
let ref;
let count = 0;
try {
// 判斷傳入的this._value是否為一個thanable // check if this._value a thenable ref = this._value &
&
this._value.then;

} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
return this._handleNextTick();

} if (this.state !== State.rejecting &
&
(typeof this._value === 'object' || typeof this._value === 'function') &
&
typeof ref === 'function') {
// add a then function to get the status of the promise // 在原有TypeScriptPromise後增加一個then函式用來判斷原有promise的狀態 ...
} else {
try {
if (this.state === State.resolving &
&
typeof this.fn === 'function') {
this._value = this.fn.call(getThis(), this._value);

} else if (this.state === State.rejecting &
&
typeof this.er === 'function') {
this._value = this.er.call(getThis(), this._reason);
this.state = 1;

}
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();

} // if promise === x, use TypeError to reject promise // 如果promise和x指向同一個物件,那麼用TypeError作為原因拒絕promise if (this._value === this) {
this.state = State.rejecting;
this._reason = new TypeError();
this._value = void 0;

} this._finishThisTypeScriptPromise();

}
}
}複製程式碼

getThis

在Promise/A+規範中規定:我們在呼叫fner兩個回撥函式時,this的指向有限制。在嚴格模式下,this的值應該為undefined;在寬鬆模式下時,this的值應該為global

因此,我們還需要提供一個getThis函式用於處理上述情況。具體程式碼如下:

class Promise { 
...
}function getThis() {
return this;

}複製程式碼

類靜態方法

我們通過上面說到的類方法和一些特定分支的邏輯處理,我們就已經實現了一個符合基本功能的Promise類。那麼,下面我們來看下ES6中提供的一些標準API我們如何來進行實現。具體API如下:

  • resolve
  • reject
  • all
  • race

下面我們就一個一個方法來看下。

resolve &
&
reject

首先我們來看下最簡單的resolvereject方法。

class Promise { 
public static resolve(value?) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();

} if (value instanceof TypeScriptPromise) {
return value;

} return new TypeScriptPromise((resolve) =>
{
resolve(value);

});

} public static reject(value?) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();

} return new TypeScriptPromise((resolve, reject) =>
{
reject(value);

});

}
}複製程式碼

通過上面程式碼我們可以看到,resolvereject方法基本上就是直接使用了內部的constructor方法進行Promise構建。

all

class Promise { 
public static all(arr) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();

} if (!(arr instanceof Array)) {
return TypeScriptPromise.reject(new TypeError());

} let promise = new TypeScriptPromise();
function done() {
// 統計還有多少未完成的TypeScriptPromise // count the unresolved promise let unresolvedNumber = arr.filter((element) =>
{
return element &
&
element.then;

}).length;
if (!unresolvedNumber) {
promise.resolve(arr);

} arr.map((element, index) =>
{
if (element &
&
element.then) {
element.then((value) =>
{
arr[index] = value;
done();
return value;

});

}
});

} done();
return promise;

}
}複製程式碼

下面我們根據上面的程式碼來簡單說下all函式的基本思路。

首先我們需要先建立一個新的Promise用於返回,保證後面使用者呼叫then函式進行後續邏輯處理時可以設定新Promise的fner這兩個回撥函式。

然後,我們怎麼獲取上面Promise陣列中每一個Promise的值呢?方法很簡單,我們在前面就已經介紹過:我們呼叫了每一個Promise的then函式用來獲取當前這個Promise的值。並且,在每個Promise完成時,我們都檢查下是否所有的Promise都已經完成,如果已經完成,則觸發新Promise的狀態從pending轉換為resolving或者rejecting

race

class Promise { 
public static race(arr) {
if (TypeScriptPromise._d !== 1) {
throw TypeError();

} if (!(arr instanceof Array)) {
return TypeScriptPromise.reject(new TypeError());

} let promise = new TypeScriptPromise();
function done(value?) {
if (value) {
promise.resolve(value);

} let unresolvedNumber = arr.filter((element) =>
{
return element &
&
element.then;

}).length;
if (!unresolvedNumber) {
promise.resolve(arr);

} arr.map((element, index) =>
{
if (element &
&
element.then) {
element.then((value) =>
{
arr[index] = value;
done(value);
return value;

});

}
});

} done();
return promise;

}
}複製程式碼

race的思路與all基本一致。只是我們在處理函式上不同。當我們只要檢測到陣列中的Promise有一個已經轉換到了resolve或者rejected狀態(通過沒有then函式來進行判斷)時,就會立即出發新建立的Promise示例的狀態從pending轉換為resolving或者rejecting

總結

我們對Promise的非同步函式執行器、常量與屬性、類方法、類靜態方法進行了逐一介紹,讓大家對整個Promise的構造和宣告週期有了一個深度的理解和認知。在整個開發中需要注意到的一些關鍵點和細節,我在上面也一一說明了。大家只需要按照這個思路,對照Promise/A+規範就能夠完成一個符合規範的Promise庫。

最後,給大家提供一個Promise/A+測試工具,大家實現了自己的Promise後,可以使用這個工具來測試是否完全符合整個Promise/A+規範。當然,大家如果想使用我的現成程式碼,也歡迎大家使用我的程式碼Github/typescript-proimse

來源:https://juejin.im/post/5c2c718ce51d4558bf39860d

相關文章