研究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