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物件的方式來進行非同步掛起,摘錄原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Deferred.next_faster_way_Image = ((typeof window === 'object') && (typeof (Image) != "undefined") && !window.opera && document.addEventListener) && function (fun) { // Modern Browsers var d = new Deferred(); var img = new Image(); var handler = function () { d.canceller(); d.call(); }; //進行非同步掛起 img.addEventListener("load", handler, false); img.addEventListener("error", handler, false); d.canceller = function () { img.removeEventListener("load", handler, false); img.removeEventListener("error", handler, false); }; img.src = "data:image/png," + Math.random(); if (fun) d.callback.ok = fun; return d; }; |
Deferred物件的靜態方法 – Deferred.next()原始碼:
1 2 3 4 5 |
Deferred.next = Deferred.next_faster_way_readystatechange ||//IE下使用onreadystatechange() Deferred.next_faster_way_Image ||//現代瀏覽器下使用Image物件onload/onerror事件 Deferred.next_tick ||//Node下使用process.nextTick() Deferred.next_default;//預設使用setTimeout |
我們務必要理清Deferred.next()和Deferred.prototype.next(),這是兩種不同的東西:
- Deferred.next()的職責是壓入非同步的程式碼,並立即非同步執行的。
- Deferred.prototype.next()是從上一個Deferred物件鏈中構建的Deferred。當沒有上一個Deferred鏈的時候,它並不會執行next()中壓入的函式,它的執行繼承於上一個Deferred觸發的事件或自身事件的觸發[ call / fail ]。
摘錄原始碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
Deferred.prototype = { callback: {}, next: function (fun) {//壓入一個函式並返回新的Deferred物件 return this._post("ok", fun) }, call: function (val) {//觸發當前Deferred成功的事件 return this._fire("ok", val) }, _post: function (okng, fun) {//next()底層 this._next = new Deferred(); this._next.callback[okng] = fun; return this._next; }, _fire: function (okng, value) {//call()底層 var next = "ok"; try { //呼叫deferred物件相應的事件處理函式 value = this.callback[okng].call(this, value); } catch (e) { //丟擲異常則進入fail() next = "ng"; value = e; if (Deferred.onerror) Deferred.onerror(e); } if (Deferred.isDeferred(value)) { //在這裡,和_post()呼應,呼叫Deferred鏈的下一個Deferred物件 value._next = this._next; } else { if (this._next) this._next._fire(next, value); } return this; } } |
再一次強調,務必搞清楚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物件。
1 2 3 4 5 6 7 8 |
var defer = Deferred();//或new Deferred() //建立一個Deferred物件 defer.next(function () { console.log('ok'); }).error(function (text) { console.log(text);//=> linkFly }).fail('linkFly'); |
例項方法
Deferred.prototype.next和Deferred.prototype.call
Deferred.prototype.next()構建一個全新的Deferred物件,併為它繫結成功事件處理函式,在沒有呼叫Deferred.prototype.call()之前這個事件處理函式並不會執行。
1 2 3 4 |
var deferred = Deferred(); deferred.next(function (value) { console.log(value); // => linkFly }).call('linkFly'); |
Deferred.prototype.error和Deferred.prototype.fail
Deferred.prototype.error()構建一個全新的Deferred物件,併為它繫結失敗事件處理函式,在沒有呼叫Deferred.prototype.fail()之前這個事件處理函式並不會執行。
1 2 3 4 |
var deferred = Deferred(); deferred.error(function () { console.log('error');// => error }).fail(); |
靜態方法。Deferred所有的靜態方法,都可以使用Deferred.方法名()的方式呼叫。
Deferred.define(obj, list)
暴露靜態方法到obj上,無參的情況下obj是全域性物件:侵入性極強,但使用方便。list是一組方法,這組方法會同時註冊到obj上。
1 2 3 4 5 6 7 8 9 10 |
Deferred.define();//無參,侵入式,預設全域性物件,瀏覽器環境為window next(function () { console.log('ok'); });//靜態方法入next被註冊到了window下 var defer = {}; Deferred.define(defer);//非侵入式,Deferred的靜態方法註冊到了defer物件下 defer.next(function () { console.log('ok'); }); |
Deferred.isDeferred(obj)
判斷物件obj是否是jsDeferred物件的例項(Deferred物件)。
1 2 3 |
Deferred.define(); console.log(Deferred.isDeferred({}));//=> false console.log(Deferred.isDeferred(wait(2)));//=> true |
Deferred.call(fn[,args]*)
建立一個Deferred例項,並且觸發其成功事件。fn是成功後要執行的函式,後續的參數列示傳遞給fn的引數。
1 2 3 4 |
call(function (text) { console.log(text);//=> linkFly }, 'linkFly'); console.log('hello,world!');// => 先輸出 |
Deferred.next(fn)
建立一個Deferred例項,並且觸發其成功事件。fn是成功後要執行的函式,它等同於只有一個引數的call,即:Deferred.call(fn)
1 2 3 4 5 6 7 8 9 10 11 |
Deferred.define(); next(function () { console.log('ok'); }); console.log('hello,world!');// => 先輸出 //上面的程式碼等同於下面的程式碼 call(function () { console.log('ok'); }); console.log('hello,world!');// => 先輸出 |
Deferred.wait(time)
建立一個Deferred例項,並等待time(秒)後觸發其成功事件,下面的程式碼首先彈出”Hello,”,2秒後彈出”World!”。
1 2 3 4 5 6 7 8 |
next(function () { alert('Hello,'); return wait(2);//延遲2s後執行 }). next(function (r) { alert('World!'); }); console.log('hello,world!');// => 先輸出 |
Deferred.loop(n, fun)
迴圈執行n次fun,並將最後一次執行fun()的返回值作為Deferred例項成功事件處理函式的引數,同樣loop中迴圈執行的fun()也是非同步的。
1 2 3 4 5 6 7 8 |
loop(3, function () { console.log(count); return count++; }).next(function (value) { console.info(value);// => 2 }); //上面的程式碼也是非同步的(無阻塞的) console.info('linkFly'); |
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請求。
1 2 3 4 5 6 7 8 9 |
Deferred.define(); parallel(function () { //等待2秒後執行 return wait(2).next(function () { return 'hello,'; }); }, function () { return wait(1).next(function () { return 'world!' }); }).next(function (values) { console.log(values);// => ["hello,", "world!"] }); |
當parallel傳遞的引數是一個物件的時候,返回值則是一個物件:
1 2 3 4 5 6 7 8 9 10 |
parallel({ foo: wait(1).next(function () { return 1; }), bar: wait(2).next(function () { return 2; }) }).next(function (values) { console.log(values);// => Object { foo=1, bar=2 } }); |
和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
1 2 3 4 5 6 7 |
Deferred.define(); Deferred.earlier( wait(2).next(function () { return 'cnblog'; }), wait(1).next(function () { return 'linkFly' })//1s後執行成功 ).next(function (values) { console.log(values);// 1s後 => [undefined, "linkFly"] }); |
Deferred.repeat(n, fun)
迴圈執行fun方法n次,若fun的執行事件超過20毫秒則先將UI執行緒的控制權交出,等一會兒再執行下一輪的迴圈。
自己跑了一下,跑出問題來了…duang…求道友指點下迷津
1 2 3 4 5 6 7 8 |
Deferred.define(); repeat(10, function (i) { if (i === 6) { var starTime = new Date(); while (new Date().getTime() - starTime < 50) console.info(new Date().getTime() - starTime);//到6之後時候不應該再執行了,因為這個函式的執行超過了20ms } console.log(i); //=> 0,1,2,3,4,5,6,7,8,9 }); |
Deferred.chain(args)
chain()方法的引數比較獨特,可以接受多個引數,引數型別可以是:Function,Object,Array。
chain()方法比較難懂,它是將所有的引數構造出一條Deferred方法鏈。
例如Function型別的引數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Deferred.define(); chain( function () { console.log('start'); }, function () { console.log('linkFly'); } ); //等同於 next(function () { console.log('start'); }).next(function () { console.log('linkFly'); }); |
它通過函式名來判斷函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
chain( //函式名!=error,則預設為next function () { throw Error('error'); }, //函式名為error function error(e) { console.log(e.message); } ); //等同於 next(function () { throw Error('error'); }).error(function (e) { console.log(e.message); }); |
也支援Deferred.parallel()的方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
chain( [ function () { return wait(1); }, function () { return wait(2); } ] ).next(function () { console.log('ok'); }); //等同於 Deferred.parallel([ function () { return wait(1); }, function () { return wait(2); } ]).next(function () { console.log('ok'); }); |
當然可以組合引數:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
chain( function () { throw Error('error'); }, //函式名為error function error(e) { console.log(e.message); }, //組合Deferred.parallel()的方式 [ function () { return wait(1); }, function () { return wait(2); } ] ).next(function () { console.log('ok'); }); //等同於 next(function () { throw Error('error'); }).error(function (e) { console.log(e.message); }); Deferred.parallel([ function () { return wait(1); }, function () { return wait(2); } ]).next(function () { console.log('ok'); }); |
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。
1 2 3 4 5 6 7 8 9 |
var timeout = Deferred.connect(setTimeout, { target: window, ok: 0 }); timeout(1).next(function () { alert('after 1 sec'); }); //另外一種傳參 var timeout = Deferred.connect(window, "setTimeout"); timeout(1).next(function () { alert('after 1 sec'); }); |
Deferred.retry(retryCount, funcDeferred[ ,options])
呼叫retryCount次funcDeffered方法(返回值型別為Deferred),直到觸發成功事件或超過嘗試次數為止。
options引數是一個物件,{wait:number}指定每次呼叫等待的秒數。
注意:Deferred.retry()並不會通過Deferred.define(obj)暴露給obj,它只能通過Deferred.retry()呼叫。
1 2 3 4 5 6 7 8 9 10 |
Deferred.define(); Deferred.retry(3, function (number) {//Deferred.retry()方法是--i的方式實現的 console.log(number); return Deferred.next(function () { if (number ^ 1)//當number!=1的時候丟擲異常,表示失敗,number==1的時候則讓它成功 throw new Error('error'); }); }).next(function () { console.log('linkFly');//=>linkFly }); |
從原始碼這一行可以看到作者重點照顧的是這些方法:
1 |
Deferred.methods = ["parallel", "wait", "next", "call", "loop", "repeat", "chain"]; |
其他的方法或許作者也覺得有點勉強吧,在Deferred.define()中預設都沒有暴露那些API。
本來就想寫jsDeferred的API,結果讀完了原始碼…篇幅原因就不解讀原始碼的,有興趣的可以在下面的引用連結點過去看原始碼,不含註釋未壓縮版原始碼僅400行左右。
jsDeferred實現簡單,程式碼通俗易懂,而API切割的非常容易上手,理念也容易理解,隨著它的知名度提升進而讓JavaScript非同步程式設計備受矚目,在閱讀jsDeferred的時候,我總是在想這些前輩們當時苦苦思索走出JavaScript自留地的感覺,從現代的眼光來看,相比Promise,可能jsDeferred的實現甚至於略顯青澀。這也讓我想起了Robert Nyman前輩最初編寫getElementByClassName(),然而在當時看來,足夠豔驚世界。
隨著JavaScript的興起,現在的我們多喜歡四處扒來程式碼匆匆貼上完成我們大多數的任務,逐漸的丟失了自己思考和挖掘程式碼的能力。值得慶幸的是JavaScript正在凝結自己的精華,未來迢長路遠,與君共勉。
下一篇將會講解JavaScript非同步程式設計的特性——控制反轉。