歡迎光臨
每天分享高質量文章

JavaScript異步編程(2)- 先驅者:jsDeferred

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.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()原始碼:

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 ]。

摘錄原始碼如下:

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物件。

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()之前這個事件處理函式並不會執行。

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()之前這個事件處理函式並不會執行。

var deferred = Deferred();

deferred.error(function () {

console.log(‘error’);// => error

}).fail();

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

Deferred.define(obj, list)

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

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物件)。

Deferred.define();

console.log(Deferred.isDeferred({}));//=> false

console.log(Deferred.isDeferred(wait(2)));//=> true

Deferred.call(fn[,args]*)

創建一個Deferred實體,並且觸發其成功事件。fn是成功後要執行的函式,後續的引數表示傳遞給fn的引數。

call(function (text) {

console.log(text);//=> linkFly

}, ‘linkFly’);

console.log(‘hello,world!’);// => 先輸出

Deferred.next(fn)

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

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!”。

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()也是異步的。

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請求。

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傳遞的引數是一個物件的時候,傳回值則是一個物件:

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

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…求道友指點下迷津

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型別的引數:

Deferred.define();

chain(

function () {

console.log(‘start’);

},

function () {

console.log(‘linkFly’);

}

);

//等同於

next(function () {

console.log(‘start’);

}).next(function () {

console.log(‘linkFly’);

});

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

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()的方式:

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’);

});

當然可以組合引數:

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。

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()呼叫。

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

});

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

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異步編程的特性——控制反轉。

參考和取用

  • ^_^肥仔John – JS魔法堂:jsDeferred原始碼剖析
  • Aaron – JSDeferred 原始碼分析
  • 司徒正美 – JavaScript框架設計:jsDeferred
  • jsDeferred



來自:linkFly

鏈接:http://www.cnblogs.com/silin6/p/4309925.html



赞(0)

分享創造快樂