Promise进阶——如何实现一个Promise库
概述
從上次更新Promise/A+規(guī)范后,已經(jīng)很久沒有更新博客了。之前由于業(yè)務(wù)需要,完成了一個TypeScript語言的Promise庫。這次我們來和大家一步一步介紹下,我們?nèi)绾螌崿F(xiàn)一個符合Promise/A+規(guī)范的Promise庫。
如果對Promise/A+規(guī)范還不太了解的同學(xué),建議先看看上一篇博客——?[譯]前端基礎(chǔ)知識儲備——Promise/A+規(guī)范?。
實現(xiàn)流程
首先,我們來看下,在我實現(xiàn)的這一個Promise中,代碼由下面這幾部分組成:
- 全局異步函數(shù)執(zhí)行器
- 常量與屬性
- 類方法
- 類靜態(tài)方法
通過上面這四個部分,我們就能夠得到一個完整的Promise。這四個部分互相有關(guān)聯(lián),接下來我們一個一個模塊來看。
全局異步函數(shù)執(zhí)行器
在之前的Promiz的源碼分析的博客中我有提到過,我們?nèi)绾蝸韺崿F(xiàn)一個異步函數(shù)執(zhí)行器。通過JavaScript的執(zhí)行原理我們可以知道,如果要實現(xiàn)異步執(zhí)行相關(guān)函數(shù)的話,我們可以選擇使用宏任務(wù)和微任務(wù),這一點在Promise/A+的規(guī)范中也有提及。因此,下面我們提供了一個用宏任務(wù)來實現(xiàn)異步函數(shù)執(zhí)行器的代碼供大家參考。
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);} } 復(fù)制代碼通過上面的代碼我們可以看到,我們一共使用了setImmediate、postMessage、setTimeout這三個添加宏任務(wù)的方法來進(jìn)行一步函數(shù)執(zhí)行。
常量與屬性
說完了如何進(jìn)行異步函數(shù)的執(zhí)行,我們來看下相關(guān)的常量與屬性。在實現(xiàn)Promise之前,我們需要定義一些常量和類屬性,用于后面存儲數(shù)據(jù)。讓我們一個一個來看下。
常量
首先,Promise共有5個狀態(tài),我們需要用常量來進(jìn)行定義,具體如下:
enum State {pending = 0,resolving = 1,rejecting = 2,resolved = 3,rejected = 4 }; 復(fù)制代碼這五個常量分別對應(yīng)Promise中的5個狀態(tài),相信大家能夠從名字中理解,我們就不多講了。
屬性
在Promise中,我們需要一些屬性來存儲數(shù)據(jù)狀態(tài)和后續(xù)的Promise引用,具體如下:
class Promise {private _value;private _reason;private _next = [];public state: State = 0;public fn;public er; } 復(fù)制代碼我們對屬性進(jìn)行逐一說明:
- _value,表示在resolved狀態(tài)時,用來存儲當(dāng)前的值。
- _reason,表示在rejected狀態(tài)時,用來存儲當(dāng)前的原因。
- _next,表示當(dāng)前Promise后面跟著then函數(shù)的引用。
- fn,表示當(dāng)前Promise中的then方法的第一個回調(diào)函數(shù)。
- er,表示當(dāng)前Promise中的then方法的的第二個回調(diào)函數(shù)(即catch的第一個參數(shù),下面看catch實現(xiàn)方法就能理解)。
類方法
看完了常量與類的屬性,我們來看下類的靜態(tài)方法。
Constructor
首先,如果我們要實現(xiàn)一個Promise,我們需要一個構(gòu)造函數(shù)來初始化最初的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);}} } 復(fù)制代碼從Promise/A+的規(guī)范來看,我們可以知道,如果resolver存在并且不是一個function的話,那么我們就應(yīng)該拋出一個錯誤;否則,我們應(yīng)該將resolve和reject方法傳給resolver作為參數(shù)。
resolve && reject
那么,resolve和reject方法又是做什么的呢?這兩個方法主要是用來讓當(dāng)前的這個Promise轉(zhuǎn)換狀態(tài)的,即從pending狀態(tài)轉(zhuǎn)換為resolving或者rejecting狀態(tài)。下面讓我們來具體看下代碼:
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;} } 復(fù)制代碼從上面的代碼中我們可以看到,當(dāng)resolve或者reject方法被觸發(fā)時,我們都改變了當(dāng)前這個Proimse的狀態(tài),并且異步調(diào)用執(zhí)行了_handleNextTick方法。狀態(tài)的改變標(biāo)志著當(dāng)前的Promise已經(jīng)從pending狀態(tài)改變成了resolving或者rejecting狀態(tài),而相應(yīng)_value和_reson也表示上一個Promise傳遞給下一個Promise的數(shù)據(jù)。
那么,這個_handleNextTick方法又是做什么的呢?其實,這個方法的作用很簡單,就是用來處理當(dāng)前這個Promise后面跟著的then函數(shù)傳遞進(jìn)來的回調(diào)函數(shù)fn和er。
then && catch
在了解_handleNextTick之前,我們們先看下then函數(shù)和catch函數(shù)的實現(xiàn)。
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);} } 復(fù)制代碼因為catch函數(shù)調(diào)用就是一個then函數(shù)的別名,我們下面就只討論then函數(shù)。
在then函數(shù)執(zhí)行時,我們會創(chuàng)建一個新的Promise,然后將傳入的兩個回調(diào)函數(shù)用新的Promise的屬性保存下來。然后,先判斷當(dāng)前的Promise的狀態(tài),如果已經(jīng)是resolved或者rejected狀態(tài)時,我們立即調(diào)用新的Promise中resolve或者reject方法,讓下將當(dāng)前Promise的_value或者_(dá)reason傳遞給下一個Promise,并且觸發(fā)下一個Promise的狀態(tài)改變。如果當(dāng)前Promise的狀態(tài)仍然為pending時,那么就將這個新生成的Promise保存下來,等當(dāng)前這個Promise的狀態(tài)改變后,再觸發(fā)新的Promise變化。最后,我們返回了這個Promise的實例。
handleNextTick
看完了then函數(shù),我們就可以來看下我們提到過的handleNextTick函數(shù)。
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作為原因拒絕promiseif (this._value === this) {this.state = State.rejecting;this._reason = new TypeError();this._value = void 0;}this._finishThisTypeScriptPromise();} } 復(fù)制代碼我們先來看一個簡單版的_handleNextTick函數(shù),這樣能夠幫助我們快速理解Promise主流程。
異步觸發(fā)了_handleNextTick函數(shù)后,我們會判斷當(dāng)前用戶處于的狀態(tài),如果當(dāng)前Promise是resolving狀態(tài),我們就會調(diào)用fn函數(shù),即我們在then函數(shù)調(diào)用時給新的Promise設(shè)置的那個fn函數(shù);而如過當(dāng)前Promise是rejecting狀態(tài),我們就會調(diào)用er函數(shù)。
上面提到的getThis方法是用來獲取特定的this值,具體的規(guī)范要求我們將在稍后再進(jìn)行介紹。
通過執(zhí)行這兩個同步的fn或er函數(shù),我們能夠得到當(dāng)前Promise執(zhí)行完傳入回調(diào)后的值。在這里需要說明的是:我們在執(zhí)行fn或者er函數(shù)之前,我們在_value和_reason中存放的值,是上一個Promise傳遞下來的值。只有當(dāng)執(zhí)行完了fn或者er函數(shù)后,_value和_reason中存放的值才是我們需要傳遞給下一個Promise的值。
大家到這里可能會奇怪,我們的this指向沒有發(fā)生變化,但是為什么我們的this指向的是那個新的Promise,而不是原來的那個Promise呢?
我們可以從另外一個角度來看待這個問題:我們當(dāng)前的這個Promise是不是由上一個Promise所產(chǎn)生的呢?如果是這種情況的話,我們就可以理解,在上一個Promise產(chǎn)生當(dāng)前Promise的時候,就設(shè)置了fn和er兩個函數(shù)。
大家可能又會問,那么我們第一個Promise的fn和er這兩個參數(shù)是怎么來的呢?
那么我們就需要仔細(xì)看下上面這個邏輯了。下面我們只討論第一個Promise處于pending的情況,其余的情況與這種情形基本相同。第一個Promise因為沒有上一個Promise去設(shè)置fn和er兩個參數(shù),因此這兩個參數(shù)的值就是undefined。所以在上面的邏輯中,我們已經(jīng)排除了這種情況,直接進(jìn)入了_finishThisTypeScriptPromise函數(shù)中。
我們在這里需要特別說明下的是,有些人會認(rèn)為我們在調(diào)用then函數(shù)傳入的兩個回調(diào)函數(shù)fn和er時,當(dāng)前Promise就結(jié)束了,其實并不是這樣,我們是得到了fn或者er兩個函數(shù)的返回值,再將值傳遞給下一個Promise時,上一個Promise才會結(jié)束。關(guān)于這個邏輯我們可以看下_finishThisTypeScriptPromise函數(shù)。
finishThisTypeScriptPromise
_finishThisTypeScriptPromise函數(shù)的代碼如下:
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);});}} } 復(fù)制代碼從_finishThisTypeScriptPromise函數(shù)中我們可以看到,我們在得到了需要傳遞給下一個Promise的_value或者_(dá)reason后,利用map方法逐個調(diào)用我們保存的新生成的Promise實例,調(diào)用它的resolve方法,因此我們又觸發(fā)了這個Promise的狀態(tài)從pending轉(zhuǎn)變?yōu)閞esolving或者rejecting。
到這里我們就已經(jīng)完全了解了一個Promise從最開始的創(chuàng)建,到最后結(jié)束的整個生命周期。下面我們來看下在Promise/A+規(guī)范中提到的一些分支邏輯的處理情況。
上一個Promise傳遞的value是Thenable實例
首先,讓我們來了解下什么是Thenable實例。Thenable實例指的是屬性中有then函數(shù)的對象。Promise就是的一種特殊的Thenable對象。
下面,為了方便講解,我們將用Promise來代替Thenable進(jìn)行講解,其他的Thenable類大家可以參考類似思路進(jìn)行分析。
如果我們在傳遞給我們的_value中是一個Promise實例,那么我們必須在等待傳入的Promise狀態(tài)轉(zhuǎn)換到resolved之后,當(dāng)前的Promise才能夠繼續(xù)往下執(zhí)行,即我們從傳入的Promise中得到了一個非Thenable返回值時,我們才能用這個值來調(diào)用屬性中的fn或者er方法。
那么,我們要怎么樣才能獲取到傳入的這個Promise的返回值呢?在Promise中其實用到了一個非常巧妙的方法:因為傳入的Promise中有一個then函數(shù)(Thenable定義),因此我們就調(diào)用then函數(shù),在第一個回調(diào)函數(shù)fn中傳入獲取_value,觸發(fā)當(dāng)前的Promise繼續(xù)執(zhí)行。如果是觸發(fā)了第二個回調(diào)函數(shù)er,那么就用在er中得到的_reason來拒絕掉當(dāng)前的Promise。具體判斷邏輯如下:
class Promise {private _handleNextTick() {let ref;let count = 0;try {// 判斷傳入的this._value是否為一個thanable// check if this._value a thenableref = 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函數(shù)用來判斷原有promise的狀態(tài)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();}} } 復(fù)制代碼promise === value
在Promise/A+規(guī)范中,如果返回的_value值等于用戶自身時,需要用TypeError錯誤拒絕掉當(dāng)前的Promise。因此我們需要在_handleNextTick中加入以下判斷代碼:
class Promise {private _handleNextTick() {let ref;let count = 0;try {// 判斷傳入的this._value是否為一個thanable// check if this._value a thenableref = 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函數(shù)用來判斷原有promise的狀態(tài)...} 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作為原因拒絕promiseif (this._value === this) {this.state = State.rejecting;this._reason = new TypeError();this._value = void 0;}this._finishThisTypeScriptPromise();}} } 復(fù)制代碼getThis
在Promise/A+規(guī)范中規(guī)定:我們在調(diào)用fn和er兩個回調(diào)函數(shù)時,this的指向有限制。在嚴(yán)格模式下,this的值應(yīng)該為undefined;在寬松模式下時,this的值應(yīng)該為global。
因此,我們還需要提供一個getThis函數(shù)用于處理上述情況。具體代碼如下:
class Promise {... }function getThis() {return this; } 復(fù)制代碼類靜態(tài)方法
我們通過上面說到的類方法和一些特定分支的邏輯處理,我們就已經(jīng)實現(xiàn)了一個符合基本功能的Promise類。那么,下面我們來看下ES6中提供的一些標(biāo)準(zhǔn)API我們?nèi)绾蝸磉M(jìn)行實現(xiàn)。具體API如下:
- resolve
- reject
- all
- race
下面我們就一個一個方法來看下。
resolve && reject
首先我們來看下最簡單的resolve和reject方法。
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);});} } 復(fù)制代碼通過上面代碼我們可以看到,resolve和reject方法基本上就是直接使用了內(nèi)部的constructor方法進(jìn)行Promise構(gòu)建。
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() {// 統(tǒng)計還有多少未完成的TypeScriptPromise// count the unresolved promiselet 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;} } 復(fù)制代碼下面我們根據(jù)上面的代碼來簡單說下all函數(shù)的基本思路。
首先我們需要先創(chuàng)建一個新的Promise用于返回,保證后面用戶調(diào)用then函數(shù)進(jìn)行后續(xù)邏輯處理時可以設(shè)置新Promise的fn和er這兩個回調(diào)函數(shù)。
然后,我們怎么獲取上面Promise數(shù)組中每一個Promise的值呢?方法很簡單,我們在前面就已經(jīng)介紹過:我們調(diào)用了每一個Promise的then函數(shù)用來獲取當(dāng)前這個Promise的值。并且,在每個Promise完成時,我們都檢查下是否所有的Promise都已經(jīng)完成,如果已經(jīng)完成,則觸發(fā)新Promise的狀態(tài)從pending轉(zhuǎn)換為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;} } 復(fù)制代碼race的思路與all基本一致。只是我們在處理函數(shù)上不同。當(dāng)我們只要檢測到數(shù)組中的Promise有一個已經(jīng)轉(zhuǎn)換到了resolve或者rejected狀態(tài)(通過沒有then函數(shù)來進(jìn)行判斷)時,就會立即出發(fā)新創(chuàng)建的Promise示例的狀態(tài)從pending轉(zhuǎn)換為resolving或者rejecting。
總結(jié)
我們對Promise的異步函數(shù)執(zhí)行器、常量與屬性、類方法、類靜態(tài)方法進(jìn)行了逐一介紹,讓大家對整個Promise的構(gòu)造和聲明周期有了一個深度的理解和認(rèn)知。在整個開發(fā)中需要注意到的一些關(guān)鍵點和細(xì)節(jié),我在上面也一一說明了。大家只需要按照這個思路,對照Promise/A+規(guī)范就能夠完成一個符合規(guī)范的Promise庫。
最后,給大家提供一個Promise/A+測試工具,大家實現(xiàn)了自己的Promise后,可以使用這個工具來測試是否完全符合整個Promise/A+規(guī)范。當(dāng)然,大家如果想使用我的現(xiàn)成代碼,也歡迎大家使用我的代碼Github/typescript-proimse。
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的Promise进阶——如何实现一个Promise库的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: oracle监听启动很慢
- 下一篇: javaSE知识点汇总