JavaScript異步編程之jsdeferred原理解析
1. 前言
最近在看司徒正美的《JavaScript框架設(shè)計(jì)》,看到異步編程的那一章介紹了jsdeferred這個庫,覺得很有意思,花了幾天的時間研究了一下代碼,在此做一下分享。
異步編程是編寫js的一個很重要的理念,特別是在處理復(fù)雜應(yīng)用的時候,異步編程的技巧就至關(guān)重要。那么下面就來看看這個被稱為里程碑式的異步編程庫吧。
2. API源碼解析
2.1 構(gòu)造函數(shù)
這里使用了安全的構(gòu)造函數(shù),避免了在沒有使用new調(diào)用構(gòu)造函數(shù)時出錯的問題,提供了兩個形式倆獲取Deferred對象實(shí)例。
- function Deferred() {
 - return (this instanceof Deferred) ? this.init() : new Deferred();
 - }
 - // 方式1
 - var o1 = new Deferred();
 - // 方式2
 - var o2 = Deferred();
 
2.2 Deferred.define()
這個方法可以包裝一個對象,指定對象的方法,或者將Deferred對象的方法直接暴露在全局作用域下,這樣就可以直接使用。
- Deferred.methods = ["parallel", "wait", "next", "call", "loop", "repeat", "chain"];
 - /*
 - @Param obj 賦予該對象Deferred的屬性方法
 - @Param list 指定屬性方法
 - */
 - Deferred.define = function(obj, list){
 - if(!list)list = Deferred.methods;
 - // 獲取全局作用域的技巧,利用立即執(zhí)行函數(shù)的作用域?yàn)槿肿饔糜虻募记?nbsp;
 - if(!obj) obj = (function getGlobal(){return this})();
 - // 將屬性都掛載到obj上
 - for(var i = 0; i < list.length; i++){
 - var n = list[i];
 - obj[n] = Deferred[n];
 - }
 - return Deferred;
 - }
 - this.Deferred = Deferred;
 
2.3 異步的操作實(shí)現(xiàn)
在JSDeferred中有許多異步操作的實(shí)現(xiàn)方式,也是作為這個框架最為出彩的地方,方法依次是:
- script.onreadystatechange(針對IE5.5~8)
 - img.onerror/img.onload(針對現(xiàn)代瀏覽器的異步操作方法)
 - 針對node環(huán)境的,使用process.nextTick來實(shí)現(xiàn)異步調(diào)用(已經(jīng)過時)
 - setTimeout(default)
 
它會視瀏覽器選擇最快的API。
1).使用script的onreadystatechange事件來進(jìn)行,需要注意的是由于瀏覽器對并發(fā)請求數(shù)有限制,(IE5.5~8為2~3,IE9+和現(xiàn)代瀏覽器為6),當(dāng)并發(fā)請求數(shù)大于上限時,會讓請求的發(fā)起操作排隊(duì)執(zhí)行,導(dǎo)致延時更嚴(yán)重。代碼的思路是以150ms為一個周期,每個周期以通過setTimeout發(fā)起的異步執(zhí)行為起始,周期內(nèi)的其他異步執(zhí)行操作通過script請求實(shí)現(xiàn),如果此方法被頻繁調(diào)用的話,說明達(dá)到并發(fā)請求數(shù)上限的可能性越高,因此可以下調(diào)一下周期時間,例如設(shè)為100ms,避免因排隊(duì)導(dǎo)致的高延時。
- Deferred.next_faster_way_readystatechange = ((typeof window === "object") &&
 - (location.protocol == "http:") &&
 - !window.opera &&
 - /\bMSIE\b/.test(navigator.userAgent)) &&
 - function (fun) {
 - var d = new Deferred();
 - var t = new Date().getTime();
 - if(t - arguments.callee._prev_timeout_called < 150){
 - var cancel = false; // 因?yàn)閞eadyState會一直變化,避免重復(fù)執(zhí)行
 - var script = document.createElement("script");
 - script.type = "text/javascript";
 - // 發(fā)送一個錯誤的url,快速觸發(fā)回調(diào),實(shí)現(xiàn)異步操作
 - script.src = "data:text/javascript,";
 - script.onreadystatechange = function () {
 - if(!cancel){
 - d.canceller();
 - d.call();
 - }
 - };
 - d.canceller = function () {
 - if(!cancel){
 - cancel = true;
 - script.onreadystatechange = null;
 - document.body.removeChild(script);// 移除節(jié)點(diǎn)
 - }
 - };
 - // 不同于img,需要添加到文檔中才會發(fā)送請求
 - document.body.appendChild(script);
 - } else {
 - // 記錄或重置起始時間
 - arguments.callee._prev_timeout_called = t;
 - // 每個周期開始使用setTimeout
 - var id = setTimeout(function (){ d.call()}, 0);
 - d.canceller = function () {clearTimeout(id)};
 - }
 - if(fun)d.callback.ok = fun;
 - return d;
 - }
 
2).使用img的方式,利用src屬性報錯和綁定事件回調(diào)的方式來進(jìn)行異步操作
- Deferred.next_faster_way_Image = ((typeof window === "object") &&
 - (typeof Image != "undefined") &&
 - !window.opera && document.addEventListener) &&
 - function (fun){
 - var d = new Deffered();
 - var img = new Image();
 - var hander = 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);
 - }
 - // 賦值一個錯誤的URL
 - img.src = "data:imag/png," + Math.random();
 - if(fun) d.callback.ok = fun;
 - return d;
 - }
 
3).針對Node環(huán)境的,使用process.nextTick來實(shí)現(xiàn)異步調(diào)用
- Deferred.next_tick = (typeof process === 'object' &&
 - typeof process.nextTick === 'function') &&
 - function (fun) {
 - var d = new Deferred();
 - process.nextTick(function() { d.call() });
 - if (fun) d.callback.ok = fun;
 - return d;
 - };
 
4).setTimeout的方式,這種方式有一個觸發(fā)最小的時間間隔,在舊的IE瀏覽器中,時間間隔可能會稍微長一點(diǎn)(15ms)。
- Deferred.next_default = function (fun) {
 - var d = new Deferred();
 - var id = setTimeout(function(){
 - clearTimeout(id);
 - d.call(); // 喚起Deferred調(diào)用鏈
 - }, 0)
 - d.canceller = function () {
 - try{
 - clearTimeout(id);
 - }catch(e){}
 - };
 - if(fun){
 - d.callback.ok = fun;
 - }
 - return d;
 - }
 
默認(rèn)的順序?yàn)?nbsp;
- Deferred.next =
 - Deferred.next_faster_way_readystatechange || // 處理IE
 - Deferred.next_faster_way_Image || // 現(xiàn)代瀏覽器
 - Deferred.next_tick || // node環(huán)境
 - Deferred.next_default; // 默認(rèn)行為
 
根據(jù)JSDeferred官方的數(shù)據(jù),使用next_faster_way_readystatechange和next_faster_way_Image這兩個比原有的setTimeout異步的方式快上700%以上。
看了一下數(shù)據(jù),其實(shí)對比的瀏覽器版本都相對比較舊,在現(xiàn)代的瀏覽器中性能提升應(yīng)該就沒有那么明顯了。
2.4 原型方法
Deferred的原型方法中實(shí)現(xiàn)了
- _id 用來判斷是否是Deferred的實(shí)例,原因好像是Mozilla有個插件也叫Deferred,因此不能通過instanceof來檢測。cho45于是自定義標(biāo)志位來作檢測,并在github上提交fxxking Mozilla。
 - init 初始化,給每個實(shí)例附加一個_next和callback屬性
 - next 用于注冊調(diào)用函數(shù),內(nèi)部以鏈表的方式實(shí)現(xiàn),節(jié)點(diǎn)為Deferred實(shí)例,調(diào)用的內(nèi)部方法_post
 - error 用于注冊函數(shù)調(diào)用失敗時的錯誤信息,與next的內(nèi)部實(shí)現(xiàn)一致。
 - call 喚起next調(diào)用鏈
 - fail 喚起error調(diào)用鏈
 - cancel 執(zhí)行cancel回調(diào),只有在喚起調(diào)用鏈之前調(diào)用才有效。(調(diào)用鏈?zhǔn)菃蜗虻模瑘?zhí)行之后就不可返回)
 
- Deferred.prototype = {
 - _id : 0xe38286e381ae, // 用于判斷是否是實(shí)例的標(biāo)識位
 - init : function () {
 - this._next = null; // 一種鏈表的實(shí)現(xiàn)思路
 - this.callback = {
 - ok : Deferred.ok, // 默認(rèn)的ok回調(diào)
 - ng : Deferred.ng // 出錯時的回調(diào)
 - };
 - return this;
 - },
 - next : function (fun) {
 - return this._post("ok", fun); // 調(diào)用_post建立鏈表
 - },
 - error : function (fun) {
 - return this._post("ng", fun); // 調(diào)用_post建立鏈表
 - },
 - call : function(val) {
 - return this._fire("ok", val); // 喚起next調(diào)用鏈
 - },
 - fail : function (err) {
 - return this._fire("ng", err); // 喚起error調(diào)用鏈
 - },
 - cancel : function () {
 - (this.canceller || function () {}).apply(this);
 - return this.init(); // 進(jìn)行重置
 - },
 - _post : function (okng, fun){ // 建立鏈表
 - this._next = new Deferred();
 - this._next.callback[okng] = fun;
 - return this._next;
 - },
 - _fire : function (okng, fun){
 - var next = "ok";
 - try{
 - // 注冊的回調(diào)函數(shù)中,可能會拋出異常,用try-catch進(jìn)行捕捉
 - value = this.callback[okng].call(this, value);
 - } catch(e) {
 - next = "ng";
 - value = e; // 傳遞出錯信息
 - if (Deferred.onerror) Deferred.onerror(e); // 發(fā)生錯誤的回調(diào)
 - }
 - if (Deferred.isDeferred(value)) { // 判斷是否是Deferred的實(shí)例
 - // 這里的代碼就是給Deferred.wait方法使用的,
 - value._next = this._next;
 - } else { // 如果不是,則繼續(xù)執(zhí)行
 - if (this._next) this._next._fire(next, value);
 - }
 - return this;
 - }
 - }
 
2.5 輔助靜態(tài)方法
上面的代碼中,可以看到一些Deferred對象的方法(靜態(tài)方法),下面簡單介紹一下:
- // 默認(rèn)的成功回調(diào)
 - Deferred.ok = function (x) {return x};
 - // 默認(rèn)的失敗回調(diào)
 - Deferred.ng = function (x) {throw x};
 - // 根據(jù)_id判斷實(shí)例的實(shí)現(xiàn)
 - Deferred.isDeferred = function (obj) {
 - return !!(obj && obj._id === Deferred.prototype._id);
 - }
 
2.6 簡單小結(jié)
看到這里,我們需要停下來,看看一個簡單的例子,來理解整個流程。
Defferred對象自身有next屬性方法,在原型上也定義了next方法,需要注意這一點(diǎn),例如以下代碼:
- var o = {};
 - Deferred.define(o);
 - o.next(function fn1(){
 - console.log(1);
 - }).next(function fn2(){
 - console.log(2);
 - });
 
- o.next()是Deffered對象的屬性方法,這個方法會返回一個Defferred對象的實(shí)例,因此下一個next()則是原型上的next方法。
 - ***個next()方法將后續(xù)的代碼變成異步操作,后面的next()方法實(shí)際上是注冊調(diào)用函數(shù)。
 - 在***個next()的異步操作里面喚起后面next()的調(diào)用鏈(d.call()),開始順序的調(diào)用,換句話說就是,fn1和fn2是同步執(zhí)行的。
 
那么,如果我們希望fn1和fn2也是異步執(zhí)行,而不是同步執(zhí)行的,這就得借助Deferred.wait方法了。
2.7 wait & register
我們可以使用wait來讓fn1和fn2變成異步執(zhí)行,代碼如下:
- Deferred.next(function fn1() {
 - console.log(1)
 - }).wait(0).next(function fn2() {
 - console.log(2)
 - });
 
wait方法很有意思,在Deferred的原型上并沒有wait方法,而是在靜態(tài)方法上找到了。
- Deferred.wait = function (n) {
 - var d = new Deferred(),
 - t = new Date();
 - // 使用定時器來變成異步操作
 - var id = setTimeout(function () {
 - d.call((new Date()).getTime() - t.getTime());
 - }, n * 1000);
 - d.canceller = function () {
 - clearTimeout(id);
 - }
 - return d;
 - }
 
那么這個方法是怎么放到原型上的?原來是通過Deferred.register進(jìn)行函數(shù)轉(zhuǎn)換,綁定到原型上的。
- Deferred.register = function (name, fun){
 - this.prototype[name] = function () { // 柯里化
 - var a = arguments;
 - return this.next(function(){
 - return fun.apply(this, a);
 - });
 - }
 - };
 - // 將方法注冊到原型上
 - Deferred.register("wait", Deferred.wait);
 
我們需要思考為什么要用這種方式將wait方法register到Deferred的原型對象上去?,因?yàn)槊黠@這種方式有點(diǎn)難以理解。
結(jié)合例子,我們進(jìn)行討論,便能夠徹底地理解上述的問題。
- Deferred.next(function fn1(){ // d1
 - console.log(1);
 - })
 - .wait(1) // d2
 - .next(function fn2(){ // d3
 - console.log(2);
 - });
 
這段代碼首先會建立一個調(diào)用鏈
之后,執(zhí)行的過程為(如圖所示)
我們來看看執(zhí)行過程的幾個關(guān)鍵點(diǎn)
- 圖中的d1、d2、d3、d_wait表示在調(diào)用鏈上生成的Deferred對象的實(shí)例
 - 在調(diào)用了d2的callback.ok即包裝了wait()方法的匿名函數(shù)之后,返回了在wait()方法中生成的Deferred對象的實(shí)例d_wait,保存在變量value中,在_fire()方法中有一個if判斷
 
- if(Deferred.isDeferred(value)){
 - value._next = this._next;
 - }
 
在這里并沒有繼續(xù)往下執(zhí)行調(diào)用鏈的函數(shù),而是重新建立了一個調(diào)用鏈,此時鏈頭為d_wait,在wait()方法中使用setTimeout,使其異步執(zhí)行,使用d.call()重新喚起調(diào)用鏈。
理解了整個過程,就比較好回到上面的問題了。之所以使用register的方式是因?yàn)樵蜕系膚ait方法并非直接使用Deferred.wait,而是把Deferred.wait方法作為參數(shù),對原型上的next()方法進(jìn)行curry化,然后返回一個柯里化之后的next()方法。而Deferred.wait()其實(shí)和Deferred.next()的作用很類似,都是異步執(zhí)行接下來的操作。
2.8 并歸結(jié)果 parallel
設(shè)想一個場景,我們需要多個異步網(wǎng)絡(luò)查詢?nèi)蝿?wù),這些任務(wù)沒有依賴關(guān)系,不需要區(qū)分前后,但是需要等待所有查詢結(jié)果回來之后才能進(jìn)一步處理,那么你會怎么做?在比較復(fù)雜的應(yīng)用中,這個場景經(jīng)常會出現(xiàn),如果我們采用以下的方式(見偽代碼)
- var result = [];
 - $.ajax("task1", function(ret1){
 - result.push(ret1);
 - $.ajax("task2", function(ret2){
 - result.push(ret2);
 - // 進(jìn)行操作
 - });
 - });
 
這種方式可以,但是卻無法同時發(fā)送task1和task2(從代碼上看還以為之間有依賴關(guān)系,實(shí)際上沒有)。那怎么解決?這就是Deferred.parallel()所要解決的問題。
我們先來個簡單的例子感受一下這種并歸結(jié)果的方式。
- Deferred.parallel(function () {
 - return 1;
 - }, function () {
 - return 2;
 - }, function () {
 - return 3;
 - }).next(function (a) {
 - console.log(a); // [1,2,3]
 - });
 
在parallel()方法執(zhí)行之后,會將結(jié)果合并為一個數(shù)組,然后傳遞給next()中的callback.ok中??梢钥吹絧arallel里面都是同步的方法,先來看看parallel的源碼是如何實(shí)現(xiàn),再來看看能不能結(jié)合所學(xué)來改造實(shí)現(xiàn)我們所需要的ajax的效果。
- Deferred.parallel = function (dl) {
 - /*
 - 前面都是對參數(shù)的處理,可以接收三種形式的參數(shù)
 - 1. parallel(fn1, fn2, fn3).next()
 - 2. parallel({
 - foo : $.get("foo.html"),
 - bar : $.get("bar.html")
 - }).next(function (v){
 - v.foo // => foo.html data
 - v.bar // => bar.html data
 - });
 - 3. parallel([fn1, fn2, fn3]).next(function (v) {
 - v[0] // fn1執(zhí)行的結(jié)果
 - v[1] // fn2執(zhí)行的結(jié)果
 - v[3] // fn3執(zhí)行返回的結(jié)果
 - });
 - */
 - var isArray = false;
 - // ***種形式
 - if (arguments.length > 1) {
 - dl = Array.prototype.slice.call(arguments);
 - isArray = true;
 - // 其余兩種形式,數(shù)組,類數(shù)組
 - } else if (Array.isArray && Array.isArray(dl)
 - || typeof dl.length == "number") {
 - isArray = true;
 - }
 - var ret = new Deferred(), // 用于歸并結(jié)果的Deferred對象的實(shí)例
 - value = {}, // 收集函數(shù)執(zhí)行的結(jié)果
 - num = 0 ; // 計(jì)數(shù)器,當(dāng)為0時說明所有任務(wù)都執(zhí)行完畢
 - // 開始遍歷,這里使用for-in其實(shí)效率不高
 - for (var i in dl) {
 - // 預(yù)防遍歷了所有屬性,例如toString之類的
 - if (dl.hasOwnProperty(i)) {
 - // 利用閉包保存變量狀態(tài)
 - (function (d, i){
 - // 使用Deferred.next()開始一個異步任務(wù),并且執(zhí)行完成之后,收集結(jié)果
 - if (typeof d == "function") dl[i] = d = Deferred.next(d);
 - d.next(function (v) {
 - values[i] = v;
 - if( --num <= 0){ // 計(jì)數(shù)器為0說明所有任務(wù)已經(jīng)完成,可以返回
 - if(isArray){ // 如果是數(shù)組的話,結(jié)果可以轉(zhuǎn)換成數(shù)組
 - values.length = dl.length;
 - values = Array.prototype.slice.call(values, 0);
 - }
 - // 調(diào)用parallel().next(function(v){}),喚起調(diào)用鏈
 - ret.call(values);
 - }
 - }).error(function (e) {
 - ret.fail(e);
 - });
 - num++; // 計(jì)數(shù)器加1
 - })(d[i], i);
 - }
 - }
 - // 當(dāng)計(jì)算器為0的時候,處理可能沒有參數(shù)或者非法參數(shù)的情況
 - if (!num) {
 - Deferred.next(function () {
 - ret.call();
 - });
 - }
 - ret.canceller = function () {
 - for (var i in dl) {
 - if (dl.hasOwnProperty(i)) {
 - dl[i].cancel();
 - }
 - }
 - };
 - return ret; // 返回Deferred實(shí)例
 - };
 
結(jié)合上述知識,我們可以在parallel中使用異步方法,代碼如下
- Deferred.parallel(function fn1(){
 - var d = new Deferred();
 - $.ajax("task1", function(ret1){
 - d.call(ret1);
 - });
 - return d;
 - }, function () {
 - var d = new Deferred();
 - $.ajax("task2", function fn2(ret2) {
 - d.call(ret2)
 - });
 - return d;
 - }).next(function fn3(ret) {
 - ret[0]; // => task1返回的結(jié)果
 - ret[1]; // => task2返回的結(jié)果
 - });
 
為什么可以這樣?我們來圖解一下,加深一下理解。
我們使用了_fire中的if判斷,建立了新的調(diào)用鏈,獲得去統(tǒng)計(jì)計(jì)數(shù)函數(shù)(即parallel中--num)的控制權(quán),從而使得在parallel執(zhí)行異步的方法。
問題解決!
考慮到篇幅問題,其他的源碼分析放在了我自己的gitbook上,歡迎交流探討。
參考資料
- jsdeferred.js
 - jsDeferred API
 - JavaScript框架設(shè)計(jì)
 


















 
 
 








 
 
 
 