JavaScript非同期プログラミングのjsdeferred原理解析


1.はじめに
最近司徒正美の「JavaScriptフレームデザイン」を見ていますが、非同期プログラムの章でjsdeferredというライブラリを紹介しています.とても面白いと思います.何日間かけてコードを研究しました.ここで共有します.
非同期プログラミングはjsを編纂するための重要な理念であり、特に複雑なアプリケーションを扱う際には、非同期プログラミングの技術が重要である.では、 と呼ばれる非同期のプログラミングライブラリを見てみましょう.
2.APIソース解析
2.1コンストラクタ
ここで を使用して、newを使用してコンストラクタを起動していないときにエラーが発生することを回避し、Deferredオブジェクトの2つの形式のインスタンスを提供する.
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;
    //           ,                     
    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非同期の操作実現
JSDeferredには多くの非同期的な操作の実現方式があり、このフレームとして最も素晴らしいところです.方法は順次です.
  • script.onreadystatechange(IE 5.5~8に対して)
  • img.onerror/img.onload(現代ブラウザに対する非同期操作方法)
  • は、node環境のために、process.nextTickを使用して、非同期呼び出しを実現する(すでに古い)
  • .
  • setTimeout
  • ブラウザを見て一番早いAPIを選択します.
  • はscriptのオンリーシステムチャンレンジイベントを使用して行われており、併発要求数が上限より大きい場合は要求の開始操作を列に並べて実行させ、リードタイムがより深刻であることに注意が必要である.コードの考え方は、150 msを周期として、各サイクルはsetTimeoutによって開始される非同期実行を開始し、周期内の他の非同期実行動作は、script要求によって実現され、この方法が頻繁に起動されると、同時要求数の上限に達する可能性が高いということで、周期時間を下げることができます.例えば、100 msとします.行列による高遅延を避ける.
    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; //   readyState     ,      
    var script = document.createElement("script");
    script.type = "text/javascript";
    //        url,      ,      
    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);//     
        }
    };
    
    //    img,              
    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;
    }
  • は、Src属性エラーとバインディングイベントのフィードバックを利用して、非同期動作
    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;
    }
  • を行う.
  • は、Node環境に対するプロcess.nextTickを使用して、非同期コール
    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;
    };
  • を実現する.
  • setTimeoutの方式は、最小の時間間隔をトリガするものであり、古いIEブラウザでは、時間間隔がやや長い(15 ms)かもしれない.
    Deferred.next_default = function (fun) {
    var d = new Deferred();
    var id = setTimeout(function(){
    clearTimeout(id);
    d.call(); //   Deferred   
    }, 0)
    d.canceller = function () {
    try{
        clearTimeout(id);
    }catch(e){}
    };
    if(fun){
    d.callback.ok = fun;
    }
    return d;
    }
  • デフォルトの順序は
    Deferred.next = 
        Deferred.next_faster_way_readystatechange || //   IE
        Deferred.next_faster_way_Image || //      
        Deferred.next_tick || // node  
        Deferred.next_default; //     
    JSDeferred公式のデータによると、next_faster_way_readystatechangenext_faster_way_Imageの2つを使用すると、従来のsetTimeoutよりも非同期的な方法で700%以上速い.
    データを見てみましたが、比較的ブラウザのバージョンが古いので、現代のブラウザではそれほど性能が向上していないはずです.
    2.4原型の方法
    Deferredのプロトタイプ方法で実現しました.
  • _IDはDeferredの例かどうかを判断するために使われています.Mozilaにはプラグインがあり、Deferredとも言われていますので、instance ofでは検出できません.cho 45は、ユーザー定義のフラグビットを検出し、githubにfxxking Mozilaを提出する.
  • init初期化は、各インスタンスに_nextおよびcallback属性
  • を追加する.
  • nextは、呼び出し関数を登録するために使用され、内部はリンクで実現され、ノードはDeferredの例であり、呼び出しの内部方法_post
  • である.
  • errorは、関数呼び出しに失敗したときのエラー情報を登録するために、nextの内部実装と一致しています.
  • call next呼び出しチェーン
  • failは、error呼び出しチェーン
  • を喚起する.
  • cancelはcancelコールバックを実行し、チェーンを起動する前に起動するだけで有効です.(コールチェーンは一方向で、実行後は戻れない)
  • Deferred.prototype = {
        _id : 0xe38286e381ae, //              
        init : function () {
            this._next = null; //          
            this.callback = {
                ok : Deferred.ok, //    ok  
                ng : Deferred.ng  //       
            };
            return this;
        },
        next : function (fun) {
            return this._post("ok", fun); //   _post    
        },
        error : function (fun) {
            return this._post("ng", fun); //   _post    
        },
        call : function(val) {
            return this._fire("ok", val); //   next   
        },
        fail : function (err) {
            return this._fire("ng", err); //   error   
        },
        cancel : function () {
            (this.canceller || function () {}).apply(this);
            return this.init(); //     
        },
        _post : function (okng, fun){ //     
            this._next = new Deferred();
            this._next.callback[okng] = fun;
            return this._next;
        },
        _fire : function (okng, fun){
            var next = "ok";
            try{
                //         ,       , try-catch    
                value = this.callback[okng].call(this, value); 
            } catch(e) {
                next = "ng";
                value = e; //       
                if (Deferred.onerror) Deferred.onerror(e); //        
            }
            if (Deferred.isDeferred(value)) { //      Deferred   
                //         Deferred.wait     ,
                value._next = this._next;
            } else { //     ,     
                if (this._next) this._next._fire(next, value);
            }
            return this;
        }
    }
    2.5補助静的方法
    上記のコードの中に、いくつかのDeferredオブジェクトの方法(静的方法)が見られます.簡単に紹介します.
    //        
    Deferred.ok = function (x) {return x};
    
    //        
    Deferred.ng = function (x) {throw x};
    
    //   _id       
    Deferred.isDeferred = function (obj) {
        return !!(obj && obj._id === Deferred.prototype._id);
    }
    2.6簡単な結び目
    ここを見て、私達は止まって、簡単な例を見て、全体の流れを理解します.
    Defferredオブジェクト自体にはnext属性の方法があり、プロトタイプにはnext方法も定義されています.この点に注意してください.例えば、以下のコードです.
    var o = {};
    Deferred.define(o);
    o.next(function fn1(){
        console.log(1);
    }).next(function fn2(){
        console.log(2);
    });
  • o.next()はDefferedオブジェクトの属性方法であり、この方法はDefferredオブジェクトのインスタンスを返すので、次のnext()はプロトタイプのnext方法である.
  • 最初のnext()メソッドは、後続のコードを非同期にして動作し、後のnext()メソッドは実際に呼び出し関数を登録する.
  • は、最初のnext()の非同期動作において、後のnext()の呼び出しチェーン(d.call()を呼び出し、開始順序の呼び出し、つまり、fn 1とfn 2は同期して実行される.
  • では、fn 1とfn 2も非同期的に実行することを望むなら、Deferred.waitの方法を借りなければならない.
    2.7 wait®ister
    私たちはwaitを使ってfn 1とfn 2を非同期にして実行できます.コードは以下の通りです.
    Deferred.next(function fn1() {
        console.log(1)
    }).wait(0).next(function fn2() {
        console.log(2)
    });
    waitの方法はとても面白いです.Deferredの原型にはwaitの方法はなく、静的な方法で見つけられました.
    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によって関数変換を行い、原型に結び付けられたのです.
    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 ?を考える必要があります.明らかにこの方法はちょっと分かりにくいです.
    例を合わせて議論すれば、上記の問題を徹底的に理解することができます.
    Deferred.next(function fn1(){ // d1
        console.log(1);
    })
    .wait(1) // d2
    .next(function fn2(){ // d3
        console.log(2);
    });
    このコードはまず呼び出しチェーンを作ります.
    その後、実行するプロセスは(図に示すように)
    実行過程のいくつかの重要な点を見てみましょう.
  • 図のd 1、d 2、d 3、d_waitは、呼び出しチェーン上で生成されたDeferredオブジェクトの例
  • を示す.
  • は、d 2のcalback.okを呼び出して、wait()方法の匿名関数を包装した後、wait()方法で生成されたDeferredオブジェクトのインスタンスd_を返した.waitは、変数valueに保存し、_fire()メソッドにはif判定
    if(Deferred.isDeferred(value)){
        value._next = this._next;
    }
  • がある.
                      ,            ,     d_wait, wait()     setTimeout,      ,  d.call()       。
    
    全体の過程を理解すれば、上の問題に戻りやすいです.registerを使うのは、原型上のwait方法が直接Deferred.waitを使うのではなく、Deferred.wait方法をパラメータとして、原型上のnext()方法をcurry化して、柯里化したnext()方法に戻るからです.Deferred.wait()とDeferred.next()の役割は似ています.次の操作は非同期で行います.
    2.8結果parallel
    一つのシーンを想定して、私たちは複数の非同期ネットワーククエリタスクが必要です.これらのタスクは依存関係がなく、前後を区別する必要はありませんが、すべての検索結果が戻ってきたらさらに処理できます.どうすればいいですか?比較的複雑な応用の中で、この場面はよく現れます.もし私達は以下の方式を採用すれば(疑似コードを見ます)
    var result = [];
    $.ajax("task1", function(ret1){
        result.push(ret1);
        $.ajax("task2", function(ret2){
            result.push(ret2);
            //     
        });
    });
    このようにしてもいいですが、task1task2を同時に送信することはできません.どう解決しますか?これはDeferred.parallelの解決すべき問題です.
    まず簡単な例を見て、このような結果に戻る方法を感じてみましょう.
    Deferred.parallel(function () {
        return 1;
    }, function () {
        return 2;
    }, function () {
        return 3;
    }).next(function (a) {
        console.log(a); // [1,2,3]
    });
    parallel()メソッドが実行された後、結果を一つの配列に統合し、next()のcalback.okに伝達します.parallelの中はすべて同期の方法であることが見えます.まず、parallelのソースコードがどのように実現されているかを確認してから、学んだことに合わせて改造して、私達が必要とするajaxの効果を実現できるかを確認してください.
    Deferred.parallel = function (dl) {
        /* 
                      ,            
            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     
                    v[1] // fn2     
                    v[3] // fn3       
                });
        */
        var isArray = false;
        //      
        if (arguments.length > 1) {
            dl = Array.prototype.slice.call(arguments);
            isArray = true;
        //       ,  ,   
        } else if (Array.isArray && Array.isArray(dl) 
                    || typeof dl.length == "number") {
            isArray = true;
        }
        var ret = new Deferred(), //        Deferred     
            value = {}, //          
            num = 0 ; //    ,  0            
        
        //     ,    for-in      
        for (var i in dl) {
            //          ,  toString   
            if (dl.hasOwnProperty(i)) {
                //           
                (function (d, i){
                    //   Deferred.next()        ,        ,    
                    if (typeof d == "function") dl[i] = d = Deferred.next(d);
                    d.next(function (v) {
                        values[i] = v;
                        if( --num <= 0){ //     0          ,    
                            if(isArray){ //        ,         
                                values.length = dl.length;
                                values = Array.prototype.slice.call(values, 0);
                            }
                            //   parallel().next(function(v){}),     
                            ret.call(values);
                        }
                    }).error(function (e) {
                        ret.fail(e);
                    });
                    num++; //     1
                })(d[i], i);
            } 
        }
        
        //      0   ,                 
        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  
    };
    上記の知識を結び付けて、私達は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     
        ret[1]; // => task2     
    });
    どうしてですか?私たちは図解して理解を深めます.
    私達は_を使いましたファイアー内のif判定により、新しい呼び出しチェーンが確立され、デ統計カウント関数(つまり、パラル中のnum)の制御権が得られ、パラルで非同期的な方法が実行されるようになる.
    問題解決
    紙面の問題を考慮して、その他のソースコードの分析は私自身のgitbookに置いて、交流の探求を歓迎します.
    参考資料
  • jsdeferred.js
  • js Deferred API
  • JavaScriptフレーム設計