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

JavaScript當前有衆多實現異步編程的方式,最爲耀眼的就是ECMAScript 6規範中的Promise對象,它來自於CommonJS小組的努力:Promise/A+規範。 javascript

研究javascript的異步編程,jsDeferred也是有必要探索的:由於Promise/A+規範的制定基本上是奠基在jsDeferred上,它是javascript異步編程中里程碑式的做品。jsDeferred自身的實現也是很是有意思的。html

本文將探討項目jsDeferred的模型,帶咱們感覺一個不同的異步編程體驗和實現。java

本文內容以下:git

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

jsDeferred和Promise/A+

在上一篇文章《JavaScript異步編程(1)- ECMAScript 6的Promise對象》中,咱們討論了ECMAScript 6的Promise對象,這一篇咱們來看javascript異步編程的先驅者——jsDeferred。github

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

Promise/A+

  • Promise是基於狀態的
  • 狀態標識:pending(初始狀態)、fulfilled(成功狀態)和rejected(失敗狀態)。
  • 狀態爲單方向移動「pending->fulfilled","pending->rejected"。
  • 因爲存在狀態標識,因此支持晚事件處理的晚綁定。

jsDeferred

  • jsDeferred是基於事件的,並無狀態標識
  • 實例的成功/失敗事件是基於事件觸發而被調用
  • 由於沒有狀態標識,因此能夠屢次觸發成功/失敗事件
  • 不支持晚綁定

jsDeferred的工做模型

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

jsDeferred Model

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

jsDeferred第一次調用next有着不一樣的處理,jsDeferred在第一次調用next()的時候,會當即異步執行這個回調函數——而這個掛起異步,則視當前的環境(如瀏覽器最佳環境)選擇最優的異步掛起方案,例如現代瀏覽器下會經過建立Image對象的方式來進行異步掛起,摘錄源碼以下:api

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.parallel

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

參考和引用

做者:linkFly
聲明:嘿!你都拷走上面那麼一大段了,我以爲你應該也不介意順便拷走這一小段,但願你可以在每一次的引用中都保留這一段聲明,尊重做者的辛勤勞動成果,本文與博客園共享。
相關文章
相關標籤/搜索