分類:
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
閒話
這篇文章,一個月前就該出爐了。跳票的原因,是因為好奇標準的promise/A+規範,於是學習了es6的promise,由於興趣,又完整的學習了《ECMAScript 6入門》。
本文目的在於解析jQuery對的promise實現(即Deferred,是一種非標準的promise實現),順便剖析、挖掘觀察者模式的能力。建議讀完後參考下面這篇博文的異步編程部分,瞭解Promise、Generator、Async。
ECMAScript 6規範總結(長文慎入) http://blog.csdn.net/vbdfforever/article/details/50727462
引子
傳統的異步編程使用回調函數的形式,當回調函數中調用回調函數時,層層嵌套,且每個回調內部都需要單獨捕捉錯誤,因為執行上下文在同步執行的過程中早就消失無影,無法追溯了。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
我們需要一種新的方式,能夠解除主邏輯與回調函數間的耦合(分離嵌套),並保證執行的異步性。
有兩種思路:聲明式、命令式。對於聲明式的解決這類問題,以同步方式書寫異步代碼,甚至是錯誤捕捉,需要語言層面的解決,或者至少自己要寫一個簡單的編譯器。我們並不需要實現一個webapp,只是以工具、庫的形式存在的組件,因此只考慮在現有語法框架下,使用命令式的方式。
命令式的方法,配上鏈式調用,最直接的就是下面這種思路(回調之間都被拆分開)
- 1
- 1
由於事件等待本身不會阻塞javascipt的運行,因此圖中的step2、step3、errorFun需要被儲存,等待內部合適的時候觸發它們。發現了麼,這類似於「發佈事件,等待被訂閱觸發」的過程,即觀察者模式(也稱發佈-訂閱模式)。
下面用一個(簡單到沒啥用的)玩具代碼來演示如何實現的:
- 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指定開始執行的索引位置。
[源碼]
- 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)。
[源碼]
- 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時,也不一致。
- 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規範總結的異步編程一節。文章開頭給出了地址。