jQuery源碼解析Deferred異步對象

在工做中咱們可能會把jQuery選擇作本身項目的基礎庫,由於其提供了簡便的DOM選擇器以及封裝了不少實用的方法,好比$.ajax(),它使得咱們不用操做xhrxdr對象,直接書寫咱們的代碼邏輯便可。更爲豐富的是它在ES6沒有原生支持的那段時間,提供了Deferred對象,相似於Promise對象,支持done/fail/progress/always方法和when批處理方法,這可能在項目上幫助過你。php

ES6提供了Promise對象,但因爲它是內置C++實現的,因此你也無法看它的設計。不如咱們經過jQuery的源碼來探究其設計思路,並比較一下二者的區別。本文采用jquey-3.1.2.js版本,其中英文註釋爲原版,中文註釋爲我添加。html

jQueryajax整體設計

jQuery在內部設置了全局的ajax參數,在每個ajax請求初始化時,用傳遞的參數與默認的全局參數進行混合,並構建一個jqXHR對象(提供比原生XHR更爲豐富的方法,同時實現其原生方法),經過傳遞的參數,來判斷其是否跨域、傳遞的參數類型等,設置好相關頭部信息。同時其被初始化爲一個內置Deferred對象用於異步操做(後面講到),添加done/fail方法做爲回調。同時咱們也封裝了$.get/$.post方法來快捷調用$.ajax方法。jquery

上面提到的Deferred對象,與ES6的Promise對象相似,用於更爲方便的異步操做,多種回調以及更好的書寫方式。提供progress/fail/done方法,並分別用該對象的notify/reject/resolve方法觸發,可使用then方法快速設置三個方法,使用always添加都會執行的回調,而且提供when方法支持多個異步操做合併回調。能夠追加不一樣的回調列表,其回調列表是使用內部Callbacks對象,更方便的按照隊列的方式來進行執行。git

Callbacks回調隊列對象,用於構建易於操做的回調函數集合,在操做完成後進行執行。支持四種初始化的方式once/unique/memory/stopOnFalse,分別表明只執行依次、去重、緩存結果、鏈式調用支持終止。提供fired/locked/disabled狀態值,表明是否執行過、上鎖、禁用。提供add/remove/empty/fire/lock/disable方法操做回調函數隊列。github

主要涉及到的概念就是這三個,再也不作延伸,三個對象的設計代碼行數在1200行左右,斷斷續續看了我一週 (´ཀ`」 ∠) 。咱們從這三個倒序開始入手剖析其設計。ajax

jQuery.Callbacks對象

Callbacks對象,用於管理回調函數的多用途列表。它提供了六個主要方法:算法

  1. add: 向列表中添加回調函數
  2. remove: 移除列表中的回調函數
  3. empty: 清空列表中的回調函數
  4. fire: 依次執行列表中的回調函數
  5. lock: 對列表上鎖,禁止一切操做,清除數據,但保留緩存的環境變量(只在memory參數時有用)
  6. disable: 禁用該回調列表,全部數據清空

在初始化時,支持四個參數,用空格分割:segmentfault

  1. once: 該回調列表只執行依次
  2. memory: 緩存執行環境,在添加新回調時執行先執行一次
  3. unique: 去重,每個函數均不一樣(指的是引用地址)
  4. stopOnFalse: 在調用中,若是前一個函數返回false,中斷列表的後續執行

咱們來看下其實例使用:api

let cl = $.Callbacks('once memory unique stopOnFalse');
fn1 = function (data) {
    console.log(data);
};
fn2 = function (data) {
    console.log('fn2 say:', data);
    return false;
};
cl.add(fn1);
cl.fire('Nicholas');    // Nicholas
// 因爲咱們使用memory參數,保存了執行環境,在添加新的函數時自動執行一次
cl.add(fn2);    // fn2 say: Nicholas
// 因爲咱們使用once參數,因此只能執行(fire)一次,此處無任何輸出
cl.fire('Lee');

// 後面咱們假設這裏沒有傳入once參數,每次fire均可以執行

cl.fire('Lee');    // Lee    fn2 say: Lee
// 清空列表
cl.empty();
cl.add(fn2, fn1);
// 因爲咱們設置了stopOnFalse,而fn2返回了false,則後添加的fn1不會執行
cl.fire('Nicholas');    // fn2 say: Nicholas
// 上鎖cl,禁用其操做,清除數據,可是咱們添加了memory參數,它依然會對後續添加的執行一次
cl.lock();
// 無響應
cl.fire();
cl.add(fn2);    // fn2 say: Nicholas
// 禁用cl,禁止一切操做,清除數據
cl.disable();

除了上面所說的主要功能,還提供has/locked/disabled/fireWith/fired等輔助函數。跨域

其全部源碼實現及註釋爲:

jQuery.Callbacks = function( options ) {
    options = typeof options === "string" ?
        // 將字符串中空格分割的子串,轉換爲值全爲true的對象屬性
        createOptions( options ) :
        jQuery.extend( {}, options );

    var // Flag to know if list is currently firing
        firing,

        // Last fire value for non-forgettable lists
        memory,

        // Flag to know if list was already fired
        fired,

        // Flag to prevent firing
        locked,

        // Actual callback list
        list = [],

        // Queue of execution data for repeatable lists
        queue = [],

        // Index of currently firing callback (modified by add/remove as needed)
        firingIndex = -1,

        // Fire callbacks
        fire = function() {

            // Enforce single-firing
            locked = locked || options.once;

            // Execute callbacks for all pending executions,
            // respecting firingIndex overrides and runtime changes
            fired = firing = true;
            // 爲quene隊列中不一樣的[context, args]執行list回調列表,執行過程當中會判斷stopOnFalse中間中斷
            for ( ; queue.length; firingIndex = -1 ) {
                memory = queue.shift();
                while ( ++firingIndex < list.length ) {

                    // Run callback and check for early termination
                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
                        options.stopOnFalse ) {

                        // Jump to end and forget the data so .add doesn't re-fire
                        firingIndex = list.length;
                        memory = false;
                    }
                }
            }

            // Forget the data if we're done with it
            if ( !options.memory ) {
                memory = false;
            }

            firing = false;

            // Clean up if we're done firing for good
            // 若是再也不執行了,就將保存回調的list清空,對內存更好
            if ( locked ) {

                // Keep an empty list if we have data for future add calls
                if ( memory ) {
                    list = [];

                // Otherwise, this object is spent
                } else {
                    list = "";
                }
            }
        },

        // Actual Callbacks object
        self = {

            // Add a callback or a collection of callbacks to the list
            add: function() {
                if ( list ) {

                    // If we have memory from a past run, we should fire after adding
                    // 若是咱們選擇緩存執行環境,會在新添加回調時執行一次保存的環境
                    if ( memory && !firing ) {
                        firingIndex = list.length - 1;
                        queue.push( memory );
                    }

                    ( function add( args ) {
                        jQuery.each( args, function( _, arg ) {
                            // 若是是函數,則判斷是否去重,若是爲類數組,則遞歸執行該內部函數
                            if ( jQuery.isFunction( arg ) ) {
                                if ( !options.unique || !self.has( arg ) ) {
                                    list.push( arg );
                                }
                            } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) {

                                // Inspect recursively
                                add( arg );
                            }
                        } );
                    } )( arguments );

                    if ( memory && !firing ) {
                        fire();
                    }
                }
                return this;
            },

            // Remove a callback from the list
            // 移除全部的相同回調,並同步將firingIndex-1
            remove: function() {
                jQuery.each( arguments, function( _, arg ) {
                    var index;
                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                        list.splice( index, 1 );

                        // Handle firing indexes
                        if ( index <= firingIndex ) {
                            firingIndex--;
                        }
                    }
                } );
                return this;
            },

            // Check if a given callback is in the list.
            // If no argument is given, return whether or not list has callbacks attached.
            // 檢查是否存在該函數,若是不傳遞參數,則返回是否有回調函數
            has: function( fn ) {
                return fn ?
                    jQuery.inArray( fn, list ) > -1 :
                    list.length > 0;
            },

            // Remove all callbacks from the list
            empty: function() {
                if ( list ) {
                    list = [];
                }
                return this;
            },

            // Disable .fire and .add
            // Abort any current/pending executions
            // Clear all callbacks and values
            // 置locked爲[],即!![] === true,同時將隊列和列表都清空,即禁用了該回調集合
            disable: function() {
                locked = queue = [];
                list = memory = "";
                return this;
            },
            disabled: function() {
                return !list;
            },

            // Disable .fire
            // Also disable .add unless we have memory (since it would have no effect)
            // Abort any pending executions
            // 不容許執行,但若是有緩存,則咱們容許添加後在緩存的環境下執行新添加的回調
            lock: function() {
                locked = queue = [];
                if ( !memory && !firing ) {
                    list = memory = "";
                }
                return this;
            },
            locked: function() {
                return !!locked;
            },

            // Call all callbacks with the given context and arguments
            // 爲fire附帶了一個上下文來調用fire函數,
            fireWith: function( context, args ) {
                if ( !locked ) {
                    args = args || [];
                    args = [ context, args.slice ? args.slice() : args ];
                    queue.push( args );
                    if ( !firing ) {
                        fire();
                    }
                }
                return this;
            },

            // Call all the callbacks with the given arguments
            fire: function() {
                self.fireWith( this, arguments );
                return this;
            },

            // To know if the callbacks have already been called at least once
            fired: function() {
                return !!fired;
            }
        };

    return self;
};

jQuery.Deferred對象

jQuery.Deferred對象是一個工廠函數,返回一個用於異步或同步調用的deferred對象,支持鏈式調用、回調函數隊列,而且能針對返回的狀態不一樣執行不一樣的回調。它相似於ES6提供的Promise對象,提供9個主要的方法:

  1. done: 操做成功響應時的回調函數(同步或異步,如下相同)
  2. fail: 操做失敗響應時的回調函數
  3. progress: 操做處理過程當中的回調函數
  4. resolve: 經過該方法解析該操做爲成功狀態,調用done
  5. reject: 經過該方法解析該操做爲失敗狀態,調用fail
  6. notify: 經過該方法解析該操做爲執行過程當中,調用progress
  7. then: 設置回調的簡寫,接收三個參數,分別是done/fail/progress
  8. always: 設置必須執行的回調,不管是done仍是fail
  9. promise: 返回一個受限制的Deferred對象,不容許外部直接改變完成狀態

它的實現思想是建立一個對象,包含不一樣狀態下回調函數的隊列,並在狀態爲失敗或成功後不容許再次改變。經過返回的Deferred對象進行手動調用resolve/reject/notify方法來控制流程。

看一個實例(純屬胡扯,不要當真)。咱們須要從間諜衛星返回的數據用不一樣的算法來進行解析,若是解析結果信號強度大於90%,則證實該數據有效,能夠被解析;若是強度小於10%,則證實只是宇宙噪音;不然,證實數據可能有效,換一種算法解析:

// 咱們封裝Deferred產生一個promise對象,其不能被外部手動解析,只能內部肯定最終狀態
asynPromise = function () {
    let d = $.Deferred();
    (function timer() {
        setTimeout(function () {
            // 產生隨機數,代替解析結果,來肯定本次的狀態
            let num = Math.random();
            if (num > 0.9) {
                d.resolve();    // 解析成功
            } else if (num < 0.1) {
                d.reject();    // 解析失敗
            } else {
                d.notify();    // 解析過程當中
            }
            setTimeout(timer, 1000);    // 持續不斷的解析數據
        }, 1000);
    })();
    // 若是不返回promise對象,則能夠被外部手動調整解析狀態
    return d.promise();
};

// then方法的三個參數分別表明完成、失敗、過程當中的回調函數
asynPromise().then(function () {
    console.log('resolve success');
}, function () {
    console.log('reject fail');
}, function () {
    console.log('notify progress');
});

// 本地執行結果(每一個人的不同,隨機分佈,但最後一個必定是success或fail)
notify progress
notify progress
notify progress
notify progress
notify progress
reject fail    // 後面不會再有輸出,由於一旦解析狀態爲success或fail,則不會再改變

除了上面的主要功能,還提供了notifyWith/resolveWith/rejectWith/state輔助方法。

其全部的源碼實現和註釋爲:

Deferred: function( func ) {
        var tuples = [
                // action, add listener, callbacks,
                // ... .then handlers, argument index, [final state]
                // 用於後面進行第一個參數綁定調用第二個參數,第三個和第四個參數分別是其不一樣的回調函數隊列
                [ "notify", "progress", jQuery.Callbacks( "memory" ),
                    jQuery.Callbacks( "memory" ), 2 ],
                [ "resolve", "done", jQuery.Callbacks( "once memory" ),
                    jQuery.Callbacks( "once memory" ), 0, "resolved" ],
                [ "reject", "fail", jQuery.Callbacks( "once memory" ),
                    jQuery.Callbacks( "once memory" ), 1, "rejected" ]
            ],
            state = "pending",
            promise = {
                state: function() {
                    return state;
                },
                // 同時添加done和fail句柄
                always: function() {
                    deferred.done( arguments ).fail( arguments );
                    return this;
                },
                "catch": function( fn ) {
                    return promise.then( null, fn );
                },
                then: function( onFulfilled, onRejected, onProgress ) {
                    var maxDepth = 0;
                    function resolve( depth, deferred, handler, special ) {
                        return function() {
                            var that = this,
                                args = arguments,
                                mightThrow = function() {
                                    var returned, then;

                                    // Support: Promises/A+ section 2.3.3.3.3
                                    // https://promisesaplus.com/#point-59
                                    // Ignore double-resolution attempts
                                    if ( depth < maxDepth ) {
                                        return;
                                    }

                                    returned = handler.apply( that, args );

                                    // Support: Promises/A+ section 2.3.1
                                    // https://promisesaplus.com/#point-48
                                    if ( returned === deferred.promise() ) {
                                        throw new TypeError( "Thenable self-resolution" );
                                    }

                                    // Support: Promises/A+ sections 2.3.3.1, 3.5
                                    // https://promisesaplus.com/#point-54
                                    // https://promisesaplus.com/#point-75
                                    // Retrieve `then` only once
                                    then = returned &&

                                        // Support: Promises/A+ section 2.3.4
                                        // https://promisesaplus.com/#point-64
                                        // Only check objects and functions for thenability
                                        ( typeof returned === "object" ||
                                            typeof returned === "function" ) &&
                                        returned.then;

                                    // Handle a returned thenable
                                    if ( jQuery.isFunction( then ) ) {

                                        // Special processors (notify) just wait for resolution
                                        if ( special ) {
                                            then.call(
                                                returned,
                                                resolve( maxDepth, deferred, Identity, special ),
                                                resolve( maxDepth, deferred, Thrower, special )
                                            );

                                        // Normal processors (resolve) also hook into progress
                                        } else {

                                            // ...and disregard older resolution values
                                            maxDepth++;

                                            then.call(
                                                returned,
                                                resolve( maxDepth, deferred, Identity, special ),
                                                resolve( maxDepth, deferred, Thrower, special ),
                                                resolve( maxDepth, deferred, Identity,
                                                    deferred.notifyWith )
                                            );
                                        }

                                    // Handle all other returned values
                                    } else {

                                        // Only substitute handlers pass on context
                                        // and multiple values (non-spec behavior)
                                        if ( handler !== Identity ) {
                                            that = undefined;
                                            args = [ returned ];
                                        }

                                        // Process the value(s)
                                        // Default process is resolve
                                        ( special || deferred.resolveWith )( that, args );
                                    }
                                },

                                // Only normal processors (resolve) catch and reject exceptions
                                // 只有普通的process能處理異常,其他的要進行捕獲,這裏不是特別明白,應該是由於沒有改最終的狀態吧
                                process = special ?
                                    mightThrow :
                                    function() {
                                        try {
                                            mightThrow();
                                        } catch ( e ) {

                                            if ( jQuery.Deferred.exceptionHook ) {
                                                jQuery.Deferred.exceptionHook( e,
                                                    process.stackTrace );
                                            }

                                            // Support: Promises/A+ section 2.3.3.3.4.1
                                            // https://promisesaplus.com/#point-61
                                            // Ignore post-resolution exceptions
                                            if ( depth + 1 >= maxDepth ) {

                                                // Only substitute handlers pass on context
                                                // and multiple values (non-spec behavior)
                                                if ( handler !== Thrower ) {
                                                    that = undefined;
                                                    args = [ e ];
                                                }

                                                deferred.rejectWith( that, args );
                                            }
                                        }
                                    };

                            // Support: Promises/A+ section 2.3.3.3.1
                            // https://promisesaplus.com/#point-57
                            // Re-resolve promises immediately to dodge false rejection from
                            // subsequent errors
                            if ( depth ) {
                                process();
                            } else {

                                // Call an optional hook to record the stack, in case of exception
                                // since it's otherwise lost when execution goes async
                                if ( jQuery.Deferred.getStackHook ) {
                                    process.stackTrace = jQuery.Deferred.getStackHook();
                                }
                                window.setTimeout( process );
                            }
                        };
                    }

                    return jQuery.Deferred( function( newDefer ) {

                        // progress_handlers.add( ... )
                        tuples[ 0 ][ 3 ].add(
                            resolve(
                                0,
                                newDefer,
                                jQuery.isFunction( onProgress ) ?
                                    onProgress :
                                    Identity,
                                newDefer.notifyWith
                            )
                        );

                        // fulfilled_handlers.add( ... )
                        tuples[ 1 ][ 3 ].add(
                            resolve(
                                0,
                                newDefer,
                                jQuery.isFunction( onFulfilled ) ?
                                    onFulfilled :
                                    Identity
                            )
                        );

                        // rejected_handlers.add( ... )
                        tuples[ 2 ][ 3 ].add(
                            resolve(
                                0,
                                newDefer,
                                jQuery.isFunction( onRejected ) ?
                                    onRejected :
                                    Thrower
                            )
                        );
                    } ).promise();
                },

                // Get a promise for this deferred
                // If obj is provided, the promise aspect is added to the object
                // 經過該promise對象返回一個新的擴展promise對象或自身
                promise: function( obj ) {
                    return obj != null ? jQuery.extend( obj, promise ) : promise;
                }
            },
            deferred = {};

        // Add list-specific methods
        // 給promise添加done/fail/progress事件,並添加互相的影響關係,併爲deferred對象添加3個事件函數notify/resolve/reject
        jQuery.each( tuples, function( i, tuple ) {
            var list = tuple[ 2 ],
                stateString = tuple[ 5 ];

            // promise.progress = list.add
            // promise.done = list.add
            // promise.fail = list.add
            promise[ tuple[ 1 ] ] = list.add;

            // Handle state
            // 只有done和fail有resolved和rejected狀態字段,給兩個事件添加回調,禁止再次done或者fail,鎖住progress不容許執行回調
            if ( stateString ) {
                list.add(
                    function() {

                        // state = "resolved" (i.e., fulfilled)
                        // state = "rejected"
                        state = stateString;
                    },

                    // rejected_callbacks.disable
                    // fulfilled_callbacks.disable
                    tuples[ 3 - i ][ 2 ].disable,

                    // progress_callbacks.lock
                    tuples[ 0 ][ 2 ].lock
                );
            }

            // progress_handlers.fire
            // fulfilled_handlers.fire
            // rejected_handlers.fire
            // 執行第二個回調列表
            list.add( tuple[ 3 ].fire );

            // deferred.notify = function() { deferred.notifyWith(...) }
            // deferred.resolve = function() { deferred.resolveWith(...) }
            // deferred.reject = function() { deferred.rejectWith(...) }
            // 綁定notify/resolve/reject的事件,實際執行的函數體爲加入上下文的With函數
            deferred[ tuple[ 0 ] ] = function() {
                deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments );
                return this;
            };

            // deferred.notifyWith = list.fireWith
            // deferred.resolveWith = list.fireWith
            // deferred.rejectWith = list.fireWith
            deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
        } );

        // Make the deferred a promise
        // 將deferred擴展爲一個promise對象
        promise.promise( deferred );

        // Call given func if any
        // 在建立前執行傳入的回調函數進行修改
        if ( func ) {
            func.call( deferred, deferred );
        }

        // All done!
        return deferred;
    },

jQuery.when方法

$.when()提供一種方法執行一個或多個函數的回調函數。若是傳入一個延遲對象,則返回該對象的Promise對象,能夠繼續綁定其他回調,在執行結束狀態以後也同時調用其when回調函數。若是傳入多個延遲對象,則返回一個新的master延遲對象,跟蹤全部的彙集狀態,若是都成功解析完成,才調用其when回調函數;若是有一個失敗,則所有失敗,執行錯誤回調。

其使用方法:

$.when($.ajax("/page1.php"), $.ajax("/page2.php"))
  .then(myFunc, myFailure);

其全部源碼實現和註釋爲(能力有限,有些地方實在不能準確理解執行流程):

// 給when傳遞的對象綁定master.resolve和master.reject,用於彙集多異步對象的狀態
function adoptValue( value, resolve, reject, noValue ) {
    var method;
    try {
        // Check for promise aspect first to privilege synchronous behavior
        // 若是when傳入的參數promise方法可用,則封裝promise並添加done和fail方法調用resolve和reject
        if ( value && jQuery.isFunction( ( method = value.promise ) ) ) {
            method.call( value ).done( resolve ).fail( reject );

        // Other thenables
        // 不然,就判斷傳入參數的then方法是否可用,若是可用就傳入resolve和reject方法
        } else if ( value && jQuery.isFunction( ( method = value.then ) ) ) {
            method.call( value, resolve, reject );

        // Other non-thenables
        // 若是均不可用,則爲非異步對象,直接resolve解析原值
        } else {

            // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer:
            // * false: [ value ].slice( 0 ) => resolve( value )
            // * true: [ value ].slice( 1 ) => resolve()
            resolve.apply( undefined, [ value ].slice( noValue ) );
        }

    // For Promises/A+, convert exceptions into rejections
    // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in
    // Deferred#then to conditionally suppress rejection.
    } catch ( value ) {

        // Support: Android 4.0 only
        // Strict mode functions invoked without .call/.apply get global-object context
        // 一個安卓4.0的bug,這裏不作闡釋
        reject.apply( undefined, [ value ] );
    }
}

// Deferred helper
    when: function( singleValue ) {
        var
            // count of uncompleted subordinates
            remaining = arguments.length,

            // count of unprocessed arguments
            i = remaining,

            // subordinate fulfillment data
            resolveContexts = Array( i ),
            resolveValues = slice.call( arguments ),

            // the master Deferred
            master = jQuery.Deferred(),

            // subordinate callback factory
            // 將每個響應的環境和值都保存到列表裏,在所有完成後統一傳給主Promise用於執行
            updateFunc = function( i ) {
                return function( value ) {
                    resolveContexts[ i ] = this;
                    resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
                    if ( !( --remaining ) ) {
                        master.resolveWith( resolveContexts, resolveValues );
                    }
                };
            };

        // Single- and empty arguments are adopted like Promise.resolve
        // 若是隻有一個參數,則直接將其做爲master的回調
        if ( remaining <= 1 ) {
            adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject,
                !remaining );

            // Use .then() to unwrap secondary thenables (cf. gh-3000)
            if ( master.state() === "pending" ||
                jQuery.isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) {

                return master.then();
            }
        }

        // Multiple arguments are aggregated like Promise.all array elements
        // 多參數時,進行全部參數的解析狀態聚合到master上
        while ( i-- ) {
            adoptValue( resolveValues[ i ], updateFunc( i ), master.reject );
        }

        return master.promise();
    }

後續

原本想把jQuery.DeferredjQuery.ajax以及ES6Promise對象給統一講一下,結果發現牽涉的東西太多,每個均可以單獨寫一篇文章,怕你們說太長不看,這裏先寫第一部分jQuery.Deferred吧,後續再補充另外兩篇。

jQuery的文檔很容易,使用也很方便,但其實真正想要講好很複雜,更不要說寫篇源碼分析文章了。真的是努力理解設計者的思路,爭取每行都能理解邊界條件,但踩坑太少,應用場景太少,確實有很大的疏漏,但願你們可以理解,不要偏聽一面之詞。

參考資料

  1. jQuery - Callbacks: http://api.jquery.com/jQuery....
  2. segment - jQuery Callbacks: https://segmentfault.com/a/11...
  3. jQuery-3.2.1版本
  4. jQuery - Deferred: http://api.jquery.com/jQuery....
  5. jQuery - when: http://www.jquery123.com/jQue...
  6. cnblogs - 搞懂jQuery的Promise: http://www.cnblogs.com/lvdaba...
  7. Promise A+ 規範: http://malcolmyu.github.io/ma...
相關文章
相關標籤/搜索