JavaScript非同步程式設計(2)- 先驅者:jsDeferred

發表於2015-03-24

JavaScript當前有眾多實現非同步程式設計的方式,最為耀眼的就是ECMAScript 6規範中的Promise物件,它來自於CommonJS小組的努力:Promise/A+規範。

研究javascript的非同步程式設計,jsDeferred也是有必要探索的:因為Promise/A+規範的制定基本上是奠定在jsDeferred上,它是javascript非同步程式設計中里程碑式的作品。jsDeferred自身的實現也是非常有意思的。

本文將探討專案jsDeferred的模型,帶我們感受一個不一樣的非同步程式設計體驗和實現。

本文內容如下:

  • jsDeferred和Promise/A+
  • jsDeferred的工作模型
  • jsDeferred API
  • 參考和引用

jsDeferred和Promise/A+

在上一篇文章《JavaScript非同步程式設計(1)- ECMAScript 6的Promise物件》中,我們討論了ECMAScript 6的Promise物件,這一篇我們來看javascript非同步程式設計的先驅者——jsDeferred。

jsDeferred是日本javascript高手geek cho45受MochiKit.Async.Deferred模組啟發在2007年開發(07年就在玩這個了…)的一個非同步執行類庫。我們將jsDeferred的原型和Promise/A+規範譯文戳這裡)進行對比(來自^_^肥仔John的《JS魔法堂:jsDeferred原始碼剖析》):

Promise/A+

  • Promise是基於狀態的
  • 狀態標識:pending(初始狀態)、fulfilled(成功狀態)和rejected(失敗狀態)。
  • 狀態為單方向移動“pending->fulfilled”,”pending->rejected”。
  • 由於存在狀態標識,所以支援晚事件處理的晚繫結。

jsDeferred

  • jsDeferred是基於事件的,並沒有狀態標識
  • 例項的成功/失敗事件是基於事件觸發而被呼叫
  • 因為沒有狀態標識,所以可以多次觸發成功/失敗事件
  • 不支援晚繫結

jsDeferred的工作模型

下面一張圖粗略演示了jsDeferred的工作模型。

下面涉及到jsDeferred的原始碼,對於第一次接觸的童鞋請直接拉到API一節(下一節),讀完了API再來看這裡。

jsDeferred第一次呼叫next有著不同的處理,jsDeferred在第一次呼叫next()的時候,會立即非同步執行這個回撥函式——而這個掛起非同步,則視當前的環境(如瀏覽器最佳環境)選擇最優的非同步掛起方案,例如現代瀏覽器下會通過建立Image物件的方式來進行非同步掛起,摘錄原始碼如下:

Deferred物件的靜態方法 – Deferred.next()原始碼:

我們務必要理清Deferred.next()和Deferred.prototype.next(),這是兩種不同的東西:

  • Deferred.next()的職責是壓入非同步的程式碼,並立即非同步執行的。
  • Deferred.prototype.next()是從上一個Deferred物件鏈中構建的Deferred。當沒有上一個Deferred鏈的時候,它並不會執行next()中壓入的函式,它的執行繼承於上一個Deferred觸發的事件或自身事件的觸發[ call / fail ]。

摘錄原始碼如下:

再一次強調,務必搞清楚Deferred.next()和Deferred.prototype.next()。

jsDeferred API

當我第一次知道jsDeferred API有一坨的時候,其實我是,是拒絕的。我跟你講,我拒絕,因為其實我覺得這根本要不了一坨,但正妹跟我講,jsDeferred內部會加特技,是假的一坨,是表面看起來一坨。加了特技之後,jsDeferred duang~duang~duang~,很酷,很炫,很酷炫。

jsDeferred的API眾多,因為jsDeferred把所有的非同步問題都劃分到了最小的粒子,這些API相互進行組合則可以完成逆天的非同步能力,在後續的API示例中可以看到jsDeferred API組合從而完成強大的非同步程式設計。我們在閱讀jsDeferred的API的時候應該時刻思考如果使用ES6的Promise物件又該如何去處理,閱讀應該是大腦的盛宴。
貌似沒有看到過jsDeferred的詳細的中文API文件(原API文件),就這裡順便整理一份簡單的出來(雖然它的API已經足夠通俗易懂了)。值得一提的是官網的API引導例子非常的生動和實用:
Deferred()/new Deferred ()

建構函式(constructor),建立一個Deferred物件。

例項方法
Deferred.prototype.next和Deferred.prototype.call

Deferred.prototype.next()構建一個全新的Deferred物件,併為它繫結成功事件處理函式,在沒有呼叫Deferred.prototype.call()之前這個事件處理函式並不會執行。

Deferred.prototype.error和Deferred.prototype.fail

Deferred.prototype.error()構建一個全新的Deferred物件,併為它繫結失敗事件處理函式,在沒有呼叫Deferred.prototype.fail()之前這個事件處理函式並不會執行。

靜態方法。Deferred所有的靜態方法,都可以使用Deferred.方法名()的方式呼叫。
Deferred.define(obj, list)

暴露靜態方法到obj上,無參的情況下obj是全域性物件:侵入性極強,但使用方便。list是一組方法,這組方法會同時註冊到obj上。

 

Deferred.isDeferred(obj)

判斷物件obj是否是jsDeferred物件的例項(Deferred物件)。

 

Deferred.call(fn[,args]*)

建立一個Deferred例項,並且觸發其成功事件。fn是成功後要執行的函式,後續的參數列示傳遞給fn的引數。

 

Deferred.next(fn)

建立一個Deferred例項,並且觸發其成功事件。fn是成功後要執行的函式,它等同於只有一個引數的call,即:Deferred.call(fn)

 

Deferred.wait(time)

建立一個Deferred例項,並等待time(秒)後觸發其成功事件,下面的程式碼首先彈出”Hello,”,2秒後彈出”World!”。

 

Deferred.loop(n, fun)

迴圈執行n次fun,並將最後一次執行fun()的返回值作為Deferred例項成功事件處理函式的引數,同樣loop中迴圈執行的fun()也是非同步的。

 

Deferred.parallel(dl[ ,fn]*)

把引數中非Deferred物件均轉換為Deferred物件(通過Deferred.next()),然後並行觸發dl中的Deferred例項的成功事件。
當所有Deferred物件均呼叫了成功事件處理函式後,返回的Deferred例項則觸發成功事件,並且所有返回值將被封裝為陣列作為Deferred例項的成功事件處理函式的入參。
parallel()強悍之處在於它的並歸處理,它可以將引數中多次的非同步最終並歸到一起,這一點在JavaScript ajax巢狀中尤為重要:例如同時傳送2條ajax請求,最終parallel()會並歸這2條ajax返回的結果。

parallel()進行了3次過載:

  • parallel(fn[ ,fn]*):傳入Function型別的引數,允許多個
  • parallel(Array):給定一個由Function組成的Array型別的引數
  • parallel(Object):給定一個物件,由物件中所有可列舉的Function構建Deferred

 

下面一張圖演示了Deferred.parallel的工作模型,它可以理解為合併了3次ajax請求。

當parallel傳遞的引數是一個物件的時候,返回值則是一個物件:

和jQuery.when()如出一轍。

 

Deferred.earlier(dl[ ,fn]*)

當引數中某一個Deferred物件呼叫了成功處理函式,則終止引數中其他Deferred物件的觸發的成功事件,返回的Deferred例項則觸發成功事件,並且那個觸發成功事件的函式返回值將作為Deferred例項的成功事件處理函式的入參。
注意:Deferred.earlier()並不會通過Deferred.define(obj)暴露給obj,它只能通過Deferred.earlier()呼叫。

Deferred.earlier()內部的實現和Deferred.parallel()大同小異,但值得注意的是引數,它接受的是Deferred,而不是parallel()的Function:

  • Deferred.earlier(Deferred[ ,Deferred]*):傳入Deferred型別的引數,允許多個
  • Deferred.earlier(Array):給定一個由Deferred組成的Array型別的引數
  • Deferred.earlier(Object):給定一個物件,由物件中所有可列舉的Deferred構建Deferred

 

 

Deferred.repeat(n, fun)

迴圈執行fun方法n次,若fun的執行事件超過20毫秒則先將UI執行緒的控制權交出,等一會兒再執行下一輪的迴圈。
自己跑了一下,跑出問題來了…duang…求道友指點下迷津

 

Deferred.chain(args)

chain()方法的引數比較獨特,可以接受多個引數,引數型別可以是:Function,Object,Array。
chain()方法比較難懂,它是將所有的引數構造出一條Deferred方法鏈。

例如Function型別的引數:

它通過函式名來判斷函式:

也支援Deferred.parallel()的方式:

當然可以組合引數:

 

Deferred.connect(funo, options)

將一個函式封裝為Deferred物件,其目的是融入現有的非同步程式設計。
注意:Deferred.connect()和Deferred.earlier()方法一樣,並不會通過Deferred.define(obj)暴露給obj,它只能通過Deferred.connect()呼叫。官網使用了setTimeout的例子:

Deferred.connect()有兩種過載:

  • Deferred.connect(target,string):把target上名為string指定名稱的方法包裝為Deferred物件。
  • Deferred.connect(function,Object):Object至少要有一個屬性:target。以target為this呼叫function方法,返回的是包裝後的方法,該方法返回Deferred物件。
    給包裝後的方法傳遞的引數,會傳遞給所指定的function。

 

 

Deferred.retry(retryCount, funcDeferred[ ,options])

呼叫retryCount次funcDeffered方法(返回值型別為Deferred),直到觸發成功事件或超過嘗試次數為止。
options引數是一個物件,{wait:number}指定每次呼叫等待的秒數。
注意:Deferred.retry()並不會通過Deferred.define(obj)暴露給obj,它只能通過Deferred.retry()呼叫。

從原始碼這一行可以看到作者重點照顧的是這些方法:

其他的方法或許作者也覺得有點勉強吧,在Deferred.define()中預設都沒有暴露那些API。

本來就想寫jsDeferred的API,結果讀完了原始碼…篇幅原因就不解讀原始碼的,有興趣的可以在下面的引用連結點過去看原始碼,不含註釋未壓縮版原始碼僅400行左右。

jsDeferred實現簡單,程式碼通俗易懂,而API切割的非常容易上手,理念也容易理解,隨著它的知名度提升進而讓JavaScript非同步程式設計備受矚目,在閱讀jsDeferred的時候,我總是在想這些前輩們當時苦苦思索走出JavaScript自留地的感覺,從現代的眼光來看,相比Promise,可能jsDeferred的實現甚至於略顯青澀。這也讓我想起了Robert Nyman前輩最初編寫getElementByClassName(),然而在當時看來,足夠豔驚世界。
隨著JavaScript的興起,現在的我們多喜歡四處扒來程式碼匆匆貼上完成我們大多數的任務,逐漸的丟失了自己思考和挖掘程式碼的能力。值得慶幸的是JavaScript正在凝結自己的精華,未來迢長路遠,與君共勉。
下一篇將會講解JavaScript非同步程式設計的特性——控制反轉。

參考和引用

相關文章