採用Symbol和process.nextTick實現Promise

Promise已經成為處理Node.js非同步流程的標配技術。

V8的async/await語法構築在Promise之上、處理generator的co模塊基於Promise實現。

處理http請求的axios、gulp4的構建流程、主流的測試框架mocha/ava等等都圍繞Promise為開發者量身打造。

Promise的核心特點在於非同步流程chaining、狀態存儲、then/catch條件分支明確、microtask處理等等。

為了對非同步流程的處理更有把控力,筆者借鑒了V8的Promise.js源碼和Promise A+的開源社區的實現,自己寫了個Promise實現。

在數據結構上採用了鏈表模擬callback queue,在演算法上面採用了process.nextTick來模擬microtask,在私有方法模擬上採用Symbol來實現。

下面我先給大家介紹下V8層面的Promise的實現,再分享下個人對Promise的實現方案。

如有對Promise的理解錯誤請在評論區指出。

V8 Promise的實現

下載完Node.js源碼後,可以看到Promise的代碼位置在deps/v8/src/js/promise.js。

在同一個文件夾下面還有個macros.py文件用來定義JS數據類型的方法和js到C++層面的counter。

基本可以認為macros.py只是個簡單的bridge和util,這邊不進行特別的討論。

打開promise.js文件,基本可以看到兩塊語法,一塊是標準的JS語法,一塊是在JS層面添加%標示C++實現的代碼。

後者的作用主要是在C++逐語句執行JS做的hook,將JS的control flow奪取過來。

Node.js中V8對promise.js實現的核心要點如下。

1、設置Promise的Symbol、Promise構造函數。

2、Promise狀態從pending改變的處理邏輯PromiseHandle。

3、形成Promise的數據結構NewPromiseCapability。

由於是V8環境的js代碼,所以promise.js的實現是function的堆疊,而且非同步邏輯用%做了c++的hook,所以整體上不是特別適合閱讀。

既然理順了V8的Promise展示的主要邏輯點,我就順藤摸瓜寫了個easy模式的Promise,核心邏輯大概130行。

實現Promise的主邏輯

我們謀定後動,先做好數據結構和演算法的選型。

定義數據結構

我們先看一個基本的使用形式

t//經過100ms,改變p的狀態為fulfilled、值為1ntlet p = new Promise((res,rej)=>setTimeout(res,100,1));nt//100ms後,列印1,pNext狀態為fulfilled、值為1ntlet pNext = p.then(console.log)n

如上是個簡單的Promise使用範例。

要做到fn能夠100ms後執行,p.then的作用勢必只是將fn存儲起來而已供100ms後調用。

同時fn的執行時機和p強掛鉤,所以p和pNext存在引用介面。

針對以上功能點,then的方法邏輯基本要定如下的數據結構。

  1. p能夠拿到pNext的引用
  2. pNext提供了介面給p,當p狀態改變的時候執行這個介面進行通知。

如上是我們的基本數據結構,很多同學可能會覺得很像pub/sub設計模式。

但是從數據結構的角度看,用鏈表來描述更貼切。

下面我們看下演算法層面,如何實現microtask和介面通知。

定義microtask演算法

如上由於then/catch的執行是一個microtask機制,因此要採用一個非同步api模擬這種能力。在瀏覽器環境可以選型mutationObserver,在Node環境我這邊是採用的process.nextTick。

到這邊,演算法和數據結構選型定的差不多了,下面我們就從Promise的constructor、then、catch來實現Promise。

定義constructor

constructor在形式上有一個executor函數參數,executor的參數是resolve/reject。

我們將resolve/reject定義在同一個namespace下面,並採用Symbol定義它們的方法名。

Symbol的主要好處是不可enumerate,這裡不做過多討論。

class Promise{n constructor(executor){ntif(typeof executor!==function){nttthrow new TypeError(`${executor} is not a function`)nt};ntlet resolveFn = val=>this[resolveSymbol](val);ntlet rejectFn = error=>this[rejectSymbol](error);ntdefineProperty(this,stateSymbol,pendingState)nttry{nt executor(resolveFn,resolveFn)nt}catch(err){n rejectFn(err)nt}n }n [resolveSymbol](val){ntdefineProperty(this,stateSymbol,fulfillState);ntthis.PromiseVal = val;n }n [rejectSymbol](error){ntdefineProperty(this,stateSymbol,rejectState);ntthis.PromiseVal = error;n }n}n

如上即為我們的第一步,基本上和Promise的構造函數使用方式保持了一致。

在resolve或者reject函數執行的時候,執行的功能是修改this的stateSymbol來標明它的狀態是fulfilled還是rejected.

關於代碼中的defineProperty,就是個簡單的Obj[prop]=val的細緻版本,為了專註主邏輯,這邊也不多解釋。

定義完constructor和resolve/reject函數後,我們就要考慮prototype.then/catch的邏輯了。

定義prototype.then和prototype.catch

then和catch要做兩件事,第一件是存儲microtask,另一件是如果狀態不為pending要autoRun。

由於then和catch只是一個處理fulfill,一個處理reject而已,而且then如果有第二個參數也可以兼容catch的處理邏輯。

所以我把then和catch的邏輯歸為一類,並定義[nextThenCatchSymbol]方法來處理。

class SuperPromise{n constructor(executor){ntif(typeof executor!==function){nt throw new TypeError(`${executor} is not a function`)nt};ntlet resolveFn = val=>this[resolveSymbol](val);ntet rejectFn = error=>this[rejectSymbol](error);ntdefineProperty(this,stateSymbol,pendingState)nttry{nt executor(resolveFn,resolveFn)nt}catch(err){nt rejectFn(err)nt}n }n [resolveSymbol](val){ntdefineProperty(this,stateSymbol,fulfillState);ntthis.PromiseVal = val;ntthis.RunLater()n }n [rejectSymbol](error){ntdefineProperty(this,stateSymbol,rejectState);ntthis.PromiseVal = error;ntthis.RunLater()n }n [nextThenCatchSymbol](fnArr,type){nt//將then和catch方法歸為一類ntlet method = resolve;ntlet resolveFn = fnArr[0];ntlet rejectFn = fnArr[1];ntif(type==catch){nttmethod = catch;nttrejectFn = fnArr[0];nt};ntreturn new Promise((res,rej)=>{})n }n then(fn,fn1){ntreturn this[nextThenCatchSymbol]([fn,fn1],resolve)n }n catch(fn){ntreturn [nextThenCatchSymbol]([fn],reject)n }n}n

如上[nextThenCatchSymbol]返回了一個空的Promise,沒有定義介面也沒有任何功能。為了實現chaining,必須給這個空Promise定義介面,同時將它添加進chain上一級的microtask.

於是改造這個function如下

[nextThenCatchSymbol](fnArr,type){ntlet method = resolve;ntlet resolveFn = fnArr[0];ntlet rejectFn = fnArr[1];ntif(type==catch){nttmethod = catch;nttrejectFn = fnArr[0];nt};nt//返回新的Promise,pending狀態ntlet newPromise = new SuperPromise((resolve,reject)=>{});nt//添加對外介面ntnewPromise[resolveFnSymbol]=function(val){nttlet nextValue = resolveFn(val);nttif(nextValue instanceof SuperPromise){ntttnextValue.then(val=>{nttttthis[resolveSymbol](val)nttt})ntt}else{ntttthis[resolveSymbol](nextValue)ntt}nt}ntnewPromise[rejectFnSymbol]=function(val){nttlet nextValue = rejectFn(val);nttif(nextValue instanceof SuperPromise){ntttnextValue.catch(val=>{nttttthis[rejectSymbol](val)nttt})ntt}else{ntttthis[rejectSymbol](nextValue)ntt}nt}nt//在上個Promise內部註冊microtaskntthis.microtask = {nttnewPromise nt};nt//microtask非同步執行ntthis.RunLater();ntreturn newPromisen}n

如上我們手動給newPromise指定了兩個介面[rejectFnSymbol],[resolveFnSymbol],

同時在上個Promise實例上掛了microtask,並立即執行了Runlater。

寫到這裡,大部分的數據結構已經完成,接下來就是Runlater方法的實現。

let RunLater = process.nextTick;nRunLater(){ntif(!this.microtask){nttreturn nt}ntlet state = this[stateSymbol];ntlet PromiseVal = this.PromiseVal;ntlet { newPromise } = this.microtask;ntlet hookFn= ;ntif(state == fulfillState || state == rejectState){nthookFn = state == fulfillState?resolveFnSymbol:rejectFnSymbol;ntRunLater(()=>newPromise[hookFn](PromiseVal))nt}n}n

RunLater的邏輯很簡單,就是根據當前的promise的情況來決定是執行resolve還是reject的邏輯而已。

階段性總結

寫到這邊,大部分的邏輯都已經實現完畢。

我們接下來看下如何實現state存儲和多級嵌套。

當then(fn)執行的時候,如果是個普通值就直接把promise的值改為那個值即可。

如果fn執行返回的是一個Promise,我們必須把當前的Promise掛鉤在返回的Promise上面。

要實現後者其實很簡單,只需要將當前的Promise掛在fn()的結果後面介面實現依賴關係的轉換。fn().then(val=>this[resolveSymbol])

newPromise[resolveFnSymbol]=function(val){nttlet nextValue = resolveFn(val);nttif(nextValue instanceof SuperPromise){ntttnextValue.then(val=>{nttttthis[resolveSymbol](val)nttt})ntt}else{ntttthis[resolveSymbol](nextValue)ntt}nt}n

寫到這裡,Promise的chaining , microTask,state track等基本已經實現完畢。

有興趣的同學,可以看我的源碼實現Promise實現

結語

Promise對於Node.js開發者來說是個技術標配,研究它對於非同步處理技巧的理解也是大有好處。

希望通過本文能夠給大家一個直觀的Promise實現機理。

接下來幾篇文章,我將參考Tj大神的co重新寫一個co,同時對Node.js內部bootstrap過程分項下個人心得。

歡迎關注我的知乎和我的專欄

本文首發於作者的github blog

查看Promise代碼實現簡化版請移步github promise implementation

完。

推薦閱讀:

《球球大作戰》源碼解析——(1)運行起來
nodeJS 2016年官方技術調查報告
node-webkit教程(11)Platform Service之shell
D2 - 打造高可靠與高性能的React同構解決方案
docker+webhook自動化部署實踐

TAG:Nodejs | 前端开发 | Promise |