天天看點

JS魔法堂:剖析源碼了解Promises/A規範

一、前言                              

  promises/a是由commonjs組織制定的異步模式程式設計規範,有不少庫已根據該規範及後來經改進的promises/a+規範提供了實作

  如q, bluebird, when, rsvp.js, mmdeferred, jquery.deffered()等。

  雖然上述實作庫均以promises/a+規範作為實作基準,但由于promises/a+是對promises/a規範的改進和增強,是以深入學習promises/a規範也是不可缺少的。

  本文内容主要根據以下内容進行學習後整理而成,若有纰漏請各位指正,謝謝。

  https://www.promisejs.org/

  http://wiki.commonjs.org/wiki/promises/a

  由于篇幅較長特設目錄一坨

  二、從痛點出發

  三、從感性領悟

  四、promises/a的api規範

  五、通過示例看特性

  六、官方執行個體的源碼剖析

     1. 基礎功能部分(1.1. 構造函數; 1.2. then函數的實作)

     2. 輔助功能部分(2.1. promise.resolve實作; 2.2. promise.reject實作; 2.3. promise.all實作; 2.4. promise.race實作)

  七、總結

  八、參考

二、從痛點出發                           

  js中最常見的異步程式設計方式我想應該非回調函數不可了,優點是簡單明了。但如果要實作下面的功能——非連續移動的動畫效果,那是否還那麼簡單明了呢?

  傻眼了吧!下面我們看一下使用promises/a規範異步模式的編碼方式吧!

 也許你有着“我不入地獄誰入地獄”的豪情,但如果現在需求改為循環10次執行非連續移動呢?20次呢?這時我想連地獄的門在哪也難以找到了,但promises/a的方式讓我們輕松應對。

三、 從感性領悟                            

  首先舉一個生活的例子,看看實際生活的處理邏輯和通過代的處理邏輯有什麼出入吧!

  例子:下班搭車去接小孩回家。

  直覺思維分解上述句子會得出以下任務及順序:下班->搭車->到幼稚園(國小等)接小孩->(走路)回家。可以看到這種思維方式是任務+執行順序的,絲毫沒有帶任務間的時間距離。于是同步代碼可以寫成:

   但實際執行時各任務均有可能因某些原因出現不同程度的延時,如下班時老闆突然安排一項新任務(推遲10分鐘下班),錯過班車(下一班要等10分鐘),小孩

正在搞衛生(20分鐘後搞完),回家。真實生活中我們能做的就是幹等或做點其他事等到點後再繼續之前的流程!但程式中尤其是像js這樣單線程程式是“等”

不起的,于是出現異步模式:

   當回頭再看上面這段代碼時,會發現整個流程被任務間的時間距離拉得很遠“下班------------搭車------------接小孩

-------------------回家”。回想一下真實生活中我們即使執行每個任務時均需要等待,但整個流程的抽象也隻是“下班,等,搭車,等,接

小孩,等,回家”。是以回調函數的異步模式與我們的思維模式相距甚遠,那麼如何做到即告訴程式任務間的時間距離,又從代碼結構上淡化這種時間距離感呢?而

promise就是其中一種方式了!

從開發者角度(第三人稱)來看promise作為任務間的紐帶存在,流程被抽象為“下班,promise,搭車,promise,接小

孩,promise,回家”,而任務間的時間距離則歸并到任務本身而已。從程式執行角度(第一人稱)來看promise為一個待定變量,但結果僅有兩種

——成功和失敗,于是僅對待定變量設定兩種結果的處理方式即可。

 看代碼結構被拉平了,但代碼結構的變化是表象,最根本的是任務間的時間距離被淡化了,當我們想了解工作流程時不會被時間距離分散注意力,當我們想知道各個任務的延時時隻需檢視任務定義本身即可,這就是關注點分離的一種表現哦!

  經過上述示例我想大家已經嘗到了甜頭,并希望掌握這一武器進而逃離回調地獄的折磨了。下面就一起了解promise及其api規範吧!

  1. 有限狀态機

    promise(中文:承諾)其實為一個有限狀态機,共有三種狀态:pending(執行中)、fulfilled(執行成功)和rejected(執行失敗)。

    其中pending為初始狀态,fulfilled和rejected為結束狀态(結束狀态表示promise的生命周期已結束)。

    狀态轉換關系為:pending->fulfilled,pending->rejected。

    随着狀态的轉換将觸發各種事件(如執行成功事件、執行失敗事件等)。  

  2. 構造函數

    promise({function} factory/*({function} resolve, {function} reject)*/) ,構造函數存在一個function類型的入參factory,作為唯一一個修改promise對象狀态的地方,其中factory函數的入參resolve的作用是将promise對象的狀态從pending轉換為fulfilled,而reject的作用是将promise對象的狀态從pending轉換為rejected。

    入參 void resolve({any} val)  ,

當val為非thenable對象和promise對象時則會将val作為執行成功事件處理函數的入參,若val為thenable對象時則會執行

thenable.then方法,若val為promise對象時則會将該promise對象添加到promise對象單向連結清單中。

    入參 void reject({any} reason) ,reason不管是哪種内容均直接作為執行失敗事件處理函數的入參。

    注意:關于抛異常的做法,同步模式為 throw new error("i'm synchronous way!") ,而promise規範的做法是 reject(new error("i'm asynchronous way!")); 

  3. 執行個體方法

     promise then([{function} onfulfilled[, {function} onrejected]]) ,

用于訂閱promise對象狀态轉換事件,入參onfulfilled為執行成功的事件處理函數,入參onrejected為執行失敗的事件處理函數。兩

者的傳回值均作為promise對象單向連結清單中下一個promise對象的狀态轉換事件處理函數的入參。而then方法的傳回值是一個新的promise

對象并且已添加到promise對象單向連結清單的末尾。

     promise catch({function} onrejected) ,相當于 then(null, onrejected) 。

  4. 類方法

     promise promise.resolve({any} obj) ,用于将非promise類型的入參封裝為promise對象,若obj為非thenable對象則傳回狀态為fulfilled的promise對象,對于非若入參為promise對象則直接傳回。

     promise promise.reject({any} obj) ,用于将非promise類型的入參封裝為狀态為rejected的promise對象。

     promise promise.all({array} array) ,

當array中所有promise執行個體的狀态均為fulfilled時,該方法傳回的promise對象的狀态也轉為fulfilled(執行成功事件處

理函數的入參為array數組中所有promise執行個體執行成功事件處理函數的傳回值),否則轉換為rejected。

     promise promise.race({array} array) ,

當array中所有promise執行個體的狀态出現fulfilled時,該方法傳回的promise對象的狀态也轉為fulfilled(執行成功事件處

理函數的入參為狀态為fulfilled的promise執行個體執行成功事件處理函數的傳回值),否則轉換為rejected。

  5. thenable對象

    擁有 then方法 的對象均稱為thenable對象,并且thenable對象将作為promise對象被處理。

  單看接口api是無法掌握promise/a的特性的,下面通過示例說明:

示例1——鍊式操作+執行最近的事件處理函數

該示例的結果為:hello1     hello1    hello2    hello2    error:my error!。

示例2——事件處理函數晚綁定,同樣可被觸發

該示例的結果為: hello

由于promises/a規範實際僅提供接口定義,并沒有規定具體實作細節,是以我們可以先自行作實作方式的猜想。

上述的示例1表明promise是具有鍊式操作,是以promise的内部結構應該是一個單向連結清單結構,每個節點除了自身資料外,還有一個字段用于指向下一個promise執行個體。

構造函數的具體實作可以是這樣的

而then函數的具體實作為

    基礎功能部分主要分為 構造函數 和 then函數的實作 兩部分,而 then函數的實作是了解的難點

     我們可以通過 new promise(function(resolve, reject){ resolve('hello'); }); 來跟蹤一下執行過程,發現重點在 doresolve(fn, resolve, reject) 方法調用中,該方法定義如下:

     doresovle僅僅是對resolve和reject方法進行封裝以防止同時被調用的情況而已,這時控制權到達 resolve方法 。由于resovle的入參為字元串類型,是以直接修改目前promise的狀态和儲存狀态轉換事件處理函數的實參即可(若resolve的入參為thenable對象或promise對象,則将控制權交給該對象,由該對象來設定目前promise的狀态和狀态轉換事件處理函數的實參),然後将控制權移交 finale方法 。finale方法内部會周遊deffereds數組并根據狀态調用對應的處理函數和修改promise連結清單中下一個promise對象的狀态。

 那麼deffereds數組具體是什麼呢?其實它就跟我之前猜想的thenables數組功能一緻,用于儲存狀态轉換事件處理函數和維護promise

單向連結清單(不直接存放下一個promise對象的指針,而是存放下一個promise的resovle和reject方法)的。具體資料結構如下:

    若目前promise有deffered執行個體,那麼則會執行handle函數中asap函數的函數入參

     我覺得原實作方式不夠直白,于是改成這樣:

 文字太多了,還是看圖更清楚哦!

JS魔法堂:剖析源碼了解Promises/A規範

   接下來的問題就是deffereds數組的元素是從何而來呢?那就要看看then函數了。

      then函數代碼結構上很簡單,但設計上卻很精妙。

      為了好看些,我修改了一下格式:

    源碼讀後感:

      通過閉包特性來讓連結清單後一個對象調用前一個對象的方法和變量,進而實作私有成員方法和屬性實在是過瘾。比起我猜想的實作方式通過下劃線(_)提示api調用者該屬性下的均為私有成員的做法封裝性更完整。

    輔助功能部分主要就是promise.resolve、promise.reject、promise.all、promsie.race的實作,它們均由基礎功能擴充而來。

      作用:将非promise對象轉換為promise對象,而非promise對象則被細分為兩種:thenable對象和非thenable對象。

         thenable對象的then将作為promise構造函數的工廠方法被調用

         非thenable對象(number、domstring、boolean、null、undefined等)将作為pending->fulfilled的事件處理函數的入參。

     由于源碼中加入性能優化的代碼,是以我提出核心邏輯以便分析:

    2.2. promise.reject實作

         作用:建立一個狀态為rejected的promise對象,且入參将作為onrejected函數的入參。

 作用:傳回的一個promise執行個體,且該執行個體當且僅當promise.all入參數組中所有promise元素狀态均為fulfilled時該傳回的

promise執行個體的狀态轉換為fulfilled(onfulfilled事件處理函數的入參為處理結果數組),否則轉換為rejected。

    2.4. promise.race實作

       作用:傳回一個promise對象,且入參數組中一旦某個promise對象狀态轉換為fulfilled,則該promise對象的狀态也轉換為fulfilled。

    源

碼實作的方式是即使第一個數組元素的狀态已經為fulfilled,但仍然會訂閱其他元素的onfulfilled和onrejected事件,依賴

resolve函數中的辨別位done來保證傳回的promise對象的onfulfilled函數僅執行一次。我修改為如下形式:

七、總結                               

  雖然通過promises/a規範進行異步程式設計已經舒坦不少,但該規範仍然不夠給力,于是出現了promises/a+規範。後面我們繼續探讨promises/a+規範吧!

繼續閱讀