2017年3月17日 星期五

jq_2


jQuery源碼解析(2)—— Callback、Deferred異步編程

標籤: jquerycallbackdeferred
 474人閱讀 評論(0) 收藏 舉報
 分類:
目錄(?)[+]

閒話

這篇文章,一個月前就該出爐了。跳票的原因,是因為好奇標準的promise/A+規範,於是學習了es6的promise,由於興趣,又完整的學習了《ECMAScript 6入門》
本文目的在於解析jQuery對的promise實現(即Deferred,是一種非標準的promise實現),順便剖析、挖掘觀察者模式的能力。建議讀完後參考下面這篇博文的異步編程部分,瞭解Promise、Generator、Async。
ECMAScript 6規範總結(長文慎入) http://blog.csdn.net/vbdfforever/article/details/50727462

引子

傳統的異步編程使用回調函數的形式,當回調函數中調用回調函數時,層層嵌套,且每個回調內部都需要單獨捕捉錯誤,因為執行上下文在同步執行的過程中早就消失無影,無法追溯了。
/* 回調函數 */
step1(function (error, value1) {
    step2(value1, function(error, value2) {
        try {
            // Do something with value2
        } catch(e) {
            // ...
        }
    });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
我們需要一種新的方式,能夠解除主邏輯與回調函數間的耦合(分離嵌套),並保證執行的異步性。
有兩種思路:聲明式、命令式。對於聲明式的解決這類問題,以同步方式書寫異步代碼,甚至是錯誤捕捉,需要語言層面的解決,或者至少自己要寫一個簡單的編譯器。我們並不需要實現一個webapp,只是以工具、庫的形式存在的組件,因此只考慮在現有語法框架下,使用命令式的方式。
命令式的方法,配上鏈式調用,最直接的就是下面這種思路(回調之間都被拆分開)
step1().anApi(step2).anApi(step3).catchError(errorFun)
  • 1
  • 1
由於事件等待本身不會阻塞javascipt的運行,因此圖中的step2、step3、errorFun需要被儲存,等待內部合適的時候觸發它們。發現了麼,這類似於「發佈事件,等待被訂閱觸發」的過程,即觀察者模式(也稱發佈-訂閱模式)。
下面用一個(簡單到沒啥用的)玩具代碼來演示如何實現的:
// 觀察者(堆棧,提供添加、觸發接口)
function watch() {
    var cache = [];
    return {
        done: function(callback) {
            cache.push(callback);
        },
        resolve: function() {
            for (var i=0; i<cache.length; i++) {
                cache[i].apply(this, arguments);
            }
        }
    }
}

function somethingAsync() {
    // some code...
    var lis = watch();
    事件 = function() {
        lis.resolve();
    }
    return lis;  // 返回可以綁定訂閱者的接口
}
somethingAsync().done(fn1).done(fn2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Callback

觀察者模式,可以解耦回調函數的綁定。但在這裡需要定製兩個功能:
1、遞延。對於事件,觸發的時候如果沒有監聽,就錯過了。保存觸發時的參數,添加回調時判斷該參數是否已有保存值,決定是否即時調用。
2、once。回調只能被觸發一次。
這裡需要介紹一個概念:鉤子。通過在程序不同的地方埋置鉤子,可以增加不同的特性和功能支持。同樣是觀察者模式,根據不同的需求,需要定製不同的功能。不僅是Deferred,很多時候我們都會用到觀察者模型,但是需求的功能特徵不同。jQuery抽象出Callback的目的就是儘可能挖掘觀察者模式的潛力,實現一個match多個case的強大的觀察者模式,並且考慮了循環調用的情況,不僅可以用於Deferred,還可以復用於大部分需要借用觀察者模型的其他場合,一勞永逸。比如,實現迭代器的時候,有的return false表示終止,有的卻不影響,要想兩種都支持,需要增加一個形參,而這裡的思路是通過傳入字符串參數,指定代碼中鉤子的狀態。
在Callback中,支持memory遞延(add時設置)、once單次觸發後lock鎖定狀態(fire時設置)、unique回調去重(add時設置)、stopOnfalse(fire內遍歷時判斷)。採用核心+外觀的形式,內部有一個基本的fire(還有一個基本的add,因為沒有別的接口調用直接嵌在外部調用的add內部了),和fire、fireWith外觀。增加了鎖定、禁用功能。思路是通過locked=true鎖定封住外部調用的fire相關接口(除了存在遞延memory參數,add接口仍然可以調用內部的fire操作),通過list=」「鎖定add操作。因此locked(鎖定),locked+list(禁用)。
Callback在1.12版本比1.11版本真心優雅不少,語義更清晰。list代表回調列表,當調用fire遍歷list回調列表時,回調函數本身可能又內部調用add或fire,需要考慮。當add時,沒什麼影響,只需要動態判斷list.length就好,fire時,需要先把任務存在任務列表裡,queue就相當於任務列表,裡面存著每次fire需要使用的參數(參數都是數組形式,所以肯定不是undefined)。使用firing看標記是否屬於正在fire階段。fire的過程中會持續queue.shift()然後遍歷回調。外觀fire接口,可以攔截locked的情況,不會向queue中push參數。由於遞延的效果,add中會涉及直接執行,為了減小複雜度,執行只通過內部fire接口,用firingIndex指定開始執行的索引位置。
[源碼]
// #410,Array.prototype.indexOf 兼容,下面會用到
jQuery.inArray = function( elem, arr, i ) {
    var len;

    if ( arr ) {
        if ( var indexOf = [].indexOf ) {
            return indexOf.call( arr, elem, i );
        }

        len = arr.length;
        // x?(x?x:x):x
        i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

        for ( ; i < len; i++ ) {

            // Skip accessing in sparse arrays
            if ( i in arr && arr[ i ] === elem ) {
                return i;
            }
        }
    }

    return -1;
}

// #3159,能把字符串'once memory' -> {'once': true, 'memory': true}
function createOptions( options ) {
    var object = {};
    jQuery.each( options.match( /\S+/g ) || [], function( _, flag ) {
        object[ flag ] = true;
    } );
    return object;
}

// #3189,參數為空格隔開的字符串,定製需要的觀察者模型
// 
// options -> 4種模式(鉤子),可混合
// once:  保證回調列表只被觸發一次
// memory:  能夠記憶最近一次觸發使用的參數,回調執行時都會使用該參數
// unique:  回調不會被重複添加
// stopOnFalse:  回調返回false中斷調用
jQuery.Callbacks = function( options ) {

    // 提取模式
    options = typeof options === "string" ?
        createOptions( options ) :
        jQuery.extend( {}, options );

    var // 是否正在fire觸發階段,用來判斷是外部的觸發,還是回調函數內部的嵌套觸發
        firing,

        // 記錄上次觸發時使用的參數
        memory,

        // 記錄是否已經被觸發過至少一次
        fired,

        // 鎖定外部fire相關接口
        locked,

        // 回調列表
        list = [],

        // 多次fire調用(因為可能被嵌套調用)的調用參數列表
        queue = [],

        // 回調列表list的觸發索引,也會用在指定add遞延觸發位置
        firingIndex = -1,

        // 內部核心fire接口
        fire = function() {

            // 若只能被觸發一次,此時鎖定外部fire接口
            locked = options.once;

            // 標記為已觸發、且正在觸發
            fired = firing = true;
            for ( ; queue.length; firingIndex = -1 ) {
                // fire參數列表取出第一項,開始遍歷
                memory = queue.shift();
                // 遍歷
                while ( ++firingIndex < list.length ) {

                    // 若執行後返回false,判斷是否有stopOnFalse鉤子,指定鉤子邏輯
                    if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && options.stopOnFalse ) {

                        // queue中本參數對list的遍歷到此為止,跳出
                        firingIndex = list.length;
                        // 本參數不會再有遞延效果,因為有回調已經返回了false
                        memory = false;
                    }
                }
            }

            // 若無遞延效果,queue中最後一個觸發參數不會保留
            if ( !options.memory ) {
                memory = false;
            }
            // 結束firing階段
            firing = false;

            // 如果鎖定了(比如once),外部fire封掉了,由是否有遞延指定add(會調用內部fire)是否可用,無遞延就要disable掉(locked+list)
            if ( locked ) {

                // 'once memory'
                if ( memory ) {
                    list = [];

                // disable()
                } else {
                    list = "";
                }
            }
        },

        // return self
        self = {

            // 添加回調,可以是回調數組集合。支持遞延觸發內部fire
            add: function() {
                if ( list ) {

                    // 外部顯示調用add,判斷是否是遞延觸發時機,memory推入fire列表,重置執行索引位置(遞延狀態下執行過fire,才不會重置memory)
                    if ( memory && !firing ) {
                        firingIndex = list.length - 1;
                        queue.push( memory );
                    }

                    // 通過遞歸add,支持[fn1,[fn2,[fn3,fn4]]] -> fn1,fn2,fn3,fn4
                    ( 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時,若刪除項為已執行項,要修正firingIndex位置
            remove: function() {
                jQuery.each( arguments, function( _, arg ) {
                    var index;
                    // Array.prototype.indexOf 兼容方法,從index索引位匹配
                    while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
                        list.splice( index, 1 );

                        // 修正firingIndex
                        if ( index <= firingIndex ) {
                            firingIndex--;
                        }
                    }
                } );
                return this;
            },

            // 判斷是否有指定回調,無參數則判斷回調列表是否空
            has: function( fn ) {
                return fn ?
                    // Array.prototype.indexOf 兼容方法
                    jQuery.inArray( fn, list ) > -1 :
                    list.length > 0;
            },

            // 清空list
            empty: function() {
                // 僅在list不為""時
                if ( list ) {
                    list = [];
                }
                return this;
            },

            // 禁用。list封add,locked封外部fire接口
            disable: function() {
                locked = queue = [];
                list = memory = "";
                return this;
            },
            disabled: function() {
                return !list;
            },

            // 鎖定,locked封外部fire接口,是否遞延判斷add是否可調用內部fire
            lock: function() {
                locked = true;
                // 無遞延(每次執行完memory重置為false)或沒觸發過,則直接禁用
                if ( !memory ) {
                    self.disable();
                }
                return this;
            },
            locked: function() {
                return !!locked;
            },

            // 把調用參數(memory[0]為環境,memory[1]為參數數組)推入queue,制定環境調用fire
            fireWith: function( context, args ) {
                if ( !locked ) {
                    args = args || [];
                    args = [ context, args.slice ? args.slice() : args ];
                    queue.push( args );
                    if ( !firing ) {
                        fire();
                    }
                }
                return this;
            },

            // 調用者為this
            fire: function() {
                self.fireWith( this, arguments );
                return this;
            },

            // 是否觸發過
            fired: function() {
                return !!fired;
            }
        };

    return self;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236

Deferred

Deferred是jQuery內部的promise實現,內部使用的是遞延(參數記憶)+oncelock(狀態鎖定)的觀察者模型。有三種狀態:正常時候是」notify」(沒有oncelock),成功後是」resolve」,失敗後是」reject」,每種狀態使用一個觀察者對象。當觸發成功或失敗時,相反的狀態被禁用,但notify狀態如果被觸發過則不會禁用僅僅lock鎖住(僅可以add遞延調用,不可以外部觸發)。
jQuery的實現的特點是:隨意、靈活。這也算是缺點。跟promise/A+標準反差挺大的呢。
jQuery中沒有自動的錯誤捕捉,全靠自覺,reject狀態的設置本身也不像是為了錯誤設置的,如果你代碼寫太渣,沒在合適的地方捕捉並reject,錯誤確實捉不住。標準中的reject定位就是拋出錯誤,我猜這應該是大量的實踐證明了除了成功主要是用於錯誤處理吧。而且如果真的需要處理錯誤,done也不能做到觸發下一個promise,只有then的實現可以加工一下做到。
done/fail是直接在Callback的list列表中添加回調,同步執行,回調間不會異步等待。每個then(fun)都返回一個promise,在Callback的list列表中添加一個既執行fun、又觸發then內deferred對象的回調函數,若fun返回promise對象,則在其後.done/fail( newDefer.resolve/reject ),實現異步串起回調。
Deferred也是使用了兩種編程方式的雛形,一種是把deferred當做一個對象,需要的時候deferred,另一種是用它包裹函數Deferred(fun),函數內封裝業務邏輯,優點是可以通過依賴注入的方式實現功能,可以減少暴露外部的接口,如果平常用的少可能一時不大得心應手。當然,由於Deferred兩種編程方式都使用了,減少暴露接口的特點就沒有利用了。在標準的實現中,只用了第二種方式,真正意義的隱藏了resolve/reject接口(即不是返回完整的deferred)。
[源碼]
// #3384,Deferred,使用閉包式寫法(非面向對象式,由於add/done接口暴露,所以是可以實現面向對象式的,原型上的then可以調用到add/done)
jQuery.Deferred = function( func ) {
    var tuples = [

            // action, add listener, listener list, final state
            [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ],
            [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ],
            [ "notify", "progress", jQuery.Callbacks( "memory" ) ]
        ],
        // 當前狀態
        state = "pending",

        // 不含resolve/reject接口的promise
        promise = {
            state: function() {
                return state;
            },
            always: function() {
                deferred.done( arguments ).fail( arguments );
                return this;
            },

            // 注意:每個then返回一個全新deferred對象的promise
            then: function( /* fnDone, fnFail, fnProgress */ ) {
                var fns = arguments;

                // 依賴傳入,新生成的deferred,返回deferred.promise()
                return jQuery.Deferred( function( newDefer ) {
                    jQuery.each( tuples, function( i, tuple ) {
                        // tuples中對應tuple的對應回調函數
                        var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];

                        // tuples中對應tuple的對應[ 'done' | 'fail' | 'progress' ]
                        // promise[ 'done' | 'fail' | 'progress' ]在下面被遍歷添加
                        deferred[ tuple[ 1 ] ]( function() {
                            var returned = fn && fn.apply( this, arguments );

                            // 返回promise或deferred對象時,異步觸發newDefer對應狀態
                            if ( returned && jQuery.isFunction( returned.promise ) ) {
                                returned.promise()
                                    .progress( newDefer.notify )
                                    .done( newDefer.resolve )
                                    .fail( newDefer.reject );
                            } else {

                                // 非promise對象,跟done/fail效果相當,但卻是通過觸發下一個promise的形式。若返回值存在,參數為返回值,否則為done/fail遍歷調用的argument
                                newDefer[ tuple[ 0 ] + "With" ](
                                    this === promise ? newDefer.promise() : this,
                                    fn ? [ returned ] : arguments
                                );
                            }
                        } );
                    } );
                    fns = null;
                } ).promise();
            },

            // 無參數時,返回不含resolve/reject接口的promise對象,可循環調用
            // 有參數可擴展,生成如deferred對象
            promise: function( obj ) {
                return obj != null ? jQuery.extend( obj, promise ) : promise;
            }
        },
        deferred = {};

    // 別名,不清楚是用來兼容在什麼情況[攤手]
    promise.pipe = promise.then;

    // 為promise接口添加與Callback對象交互的done(對應add)/fail/progress方法
    // 為deferred對象添加與Callback對象交互的resolve/resolveWith(對應fireWith)/reject/rejectWith
    jQuery.each( tuples, function( i, tuple ) {
        // 對應觀察者模型Callback
        var list = tuple[ 2 ],
            // 對應狀態
            stateString = tuple[ 3 ];

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

        // 'resolved' 'rejected'
        if ( stateString ) {
            list.add( function() {

                // state = [ resolved | rejected ]
                state = stateString;

            // [ reject_list | resolve_list ].disable(相反觀察者禁用); progress_list.lock(progress鎖定)
            // ^ 按位異或,0^1 = 1,1^1 = 0,(二進制寫法取不同位為1,相同位為0)
            }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
        }

        // deferred[ resolve | reject | notify ]
        deferred[ tuple[ 0 ] ] = function() {
            deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
            return this;
        };
        deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
    } );

    // 合併成最終的deferred,promise相當於deferred的一個子集。deferred.promise() -> promise
    promise.promise( deferred );

    // 執行fun,並傳入生成的deferred(對第二種編程形式的支持)
    if ( func ) {
        func.call( deferred, deferred );
    }

    // 返回deferred
    return deferred;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

when

when方法返回一個deferred的promise對象。接受多個參數,沒有promise接口的參數當做resolved狀態,當參數中全部變為resolved狀態時,會觸發when中deferred的resolve。當有一個參數變成reject,會觸發deferred的reject。當有參數調用notify時,每次調用都會執行一次。除了reject是使用觸發項的觸發參數外,resolve和reject均使用一個參數數組觸發,數組中每一項對應when中參數每一項的觸發參數,對於when參數中的非promise對象,對應的觸發參數就是它們自身。
when還考慮到只有一個參數,且帶有promise方法時,可以直接使用該參數來觸發成功操作,節省開銷,因此方法開頭做了這個優化。因此這種情況,直接由該對象接管。觸發的參數規則的不一致,個人認為很不優雅,而且updateFun裡arguments.length<=1時,也不一致。
// #3480
jQuey.when = function( subordinate /* , ..., subordinateN */ ) {
    var i = 0,
        resolveValues = slice.call( arguments ),
        length = resolveValues.length,

        // 判斷是否單參數且帶有promise方法
        remaining = length !== 1 ||
            ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,

        // 新生成Deferred對象,對單參數且帶有promise方法進行優化
        deferred = remaining === 1 ? subordinate : jQuery.Deferred(),

        updateFunc = function( i, contexts, values ) {
            // progress觸發器、resolve觸發器(根據計數器判斷是否觸發)
            return function( value ) {
                // 設置當前觸發項的環境
                contexts[ i ] = this;
                // 設置resolve/progress對應的觸發參數的數組中的該位置的參數
                values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;

                // 若觸發的是progress操作
                if ( values === progressValues ) {
                    deferred.notifyWith( contexts, values );

                // 觸發的是resolve。計數器減至0才會觸發新defer的resolve,使用resolve對應的觸發參數的數組
                } else if ( !( --remaining ) ) {
                    deferred.resolveWith( contexts, values );
                }
            };
        },

        progressValues, progressContexts, resolveContexts;

    // length為0會在if ( !remaining ){}直接調用resolve,為1時由於是參數本身,
    if ( length > 1 ) {
        // 觸發時設置的參數數組
        progressValues = new Array( length );
        progressContexts = new Array( length );
        resolveContexts = new Array( length );
        for ( ; i < length; i++ ) {
            if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
                resolveValues[ i ].promise()
                    .progress( updateFunc( i, progressContexts, progressValues ) )
                    .done( updateFunc( i, resolveContexts, resolveValues ) )
                    .fail( deferred.reject );
            } else {
                // 遇到不帶promise接口的參數計數變量-1
                --remaining;
            }
        }
    }

    // 若同步執行到此處時,已經是全resolved狀態,則直接觸發resolve
    if ( !remaining ) {
        deferred.resolveWith( resolveContexts, resolveValues );
    }

    return deferred.promise();
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
結尾:建議再參考es6規範總結的異步編程一節。文章開頭給出了地址。