-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path4-4.html
576 lines (570 loc) · 24.2 KB
/
4-4.html
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="./public/favicon.ico" />
<meta http-equiv="cache-control" content="no-cache" />
<title></title>
<link rel="stylesheet" href=" https://necolas.github.io/normalize.css/8.0.1/normalize.css" />
<link rel="stylesheet" href="./hightlight/default.min.css" />
<link rel="stylesheet" href="./css/main.css" />
<link rel="stylesheet" href="./css/copybutton.css" />
<link rel="stylesheet" href="./css/hightlight.css" />
<script src="./hightlight/hightlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEVZJDBC7Z"></script>
<script src="./js/gtag.js"></script>
</head>
<body>
<header>
<nav>
<h1>
<span id="toggle-menu"></span>
<a href="index.html"></a>
</h1>
</nav>
</header>
<main>
<aside>
<nav></nav>
</aside>
<article>
<h2>4-4 非同步函式</h2>
<h3 id="4-4-1">4-4-1 非同步函式的意義</h3>
<p>
假設我們有一段如下方的程式碼,其中fetch 請求被包裹於 getRandomArticle
函式中;當請求成功時,所回傳的 Promise 物件為已實現並附帶 JSON 格式的 body
主體資訊;若失敗,則依循標準的 fetch 已拒絕機制處理‧
</p>
<pre><code class="language-js">
function getRandomArticle(){
return fetch('/articles/random', {
headers: new Headers({
Accept:'application/json'
})
})
.then(res => res.json())
}
</code></pre>
<p>
下一段程式碼則說明 getRandomArticle 函式基本的運用方式。我們會建立一個 Promise
物件鏈結,它會擷取文章的JSON 物件,並傳遞至非同步的 renderView
的畫面描繪函式,在完成後會產生一個 HTML 頁面,接著再以該 HTML
頁面取得我們的頁面内容。為了避免無法捕捉到的錯誤,我們利用 console.error
可將所有發生的拒絕原因輸出。
</p>
<pre><code class="language-js">
getRandomArticle()
.then(model => renderView(model))
.then(html => setPageContents(html))
.then(() => console.log('Successfully changed page!'))
.catch(err => console.error (err))
</code></pre>
<p>
Promise 物件鏈結是很難除錯的:流程控制的錯誤原因很難追溯,且撰寫以 Promise
物件為基礎的程式碼流程,閱讀起來是比撰寫更加不易,常會導致後續維護上的困難。
</p>
<p>
如果是單使用單純 JavaScript
回呼函式,我們的程式碼內容會較具重複性,就如接下來的範例一樣。同時,也落入回呼困境:在非同步程式流程中的每一個步驟都再加入一層縮排,使得程式碼更難閱讀理解。
</p>
<pre><code class="language-js">
getRandomArticle( (err, model) =>{
if (err) {
return console.error(err)
}
renderView(model, (err, html) => {
if (err) {
return console.error(err)
}
setPageContents (html, err => {
if (err) {
return console.error(err)
}
console.log('Successfully changed page!')
})
})
})
</code></pre>
<p>
當然,以函式庫可以解決回呼困境,和錯誤處理的重複性·函式庫的使用可受惠於標準化回呼函式的優點,如
async 非同步,將第一個引數保留給錯誤使用。若使用它的 waterfall
方法,我們的程式又會變得更簡潔一些。
</p>
<pre><code class="language-js">
async.waterfall([
getRandomArticle,
renderView,
setPageContents
], (err, html) =>{
if (err){
return console.error(err)
}
console.log('Successfully changed page! ')
}
)
</code></pre>
<p>
我們再看一個類似的範例,但這次我們將使用產生器。下面的範例是 getRandomArticle
的重製版,此處我們使用產生器以改變 getRandomArticle 的使用方式。
</p>
<pre><code class="language-js">
function getRandomArticle(gen) {
const g = gen()
fetch('/articles/random', {
headers: new Headers({
Accept: 'application/json'
})
})
.then(res => res.json())
.then(json => g.next(json))
.catch(err => g.throw(err))
}
</code></pre>
<p>
以下的程式碼展示如何透過 yield 運算式,自 getRandomArticle 函式中擷取 json
結果值。即使看起來像是同步進行,現在其實已加入了產生器函式。當我們希望加入更多步驟時,必須大幅更動getRandomArticle
,才能夠以 yield 出產期望的結果值;並需調整產生器內容,以使用最新的結果序列。
</p>
<pre><code class="language-js">
getRandomArticle( function* printRandomArticle() {
const json = yield
// render view
})
</code></pre>
<p>
在此案例中,使用產生器可能不是達到我們期望的結果最直覺的方式:只是把複雜性搬移到其他地方而已,而我們也受限於
Promise 的使用。
</p>
<p>
除了將不直覺的語法加入其中,迭代器的程式碼也將與使用中的產生器函式高度地結合;這表示當產生器程式碼中加入了
yield 運算式,迭代器的程式碼也必須一併調整。
</p>
<p>另一個較佳的方案是使用 async 函式。</p>
<h3 id="4-4-2">4-4-2 使用 async/await</h3>
<p>
非同步函式可讓我們採用以 Promise
物件為基礎的實作,同時也受益於外觀看似同步的產生器。這種方法的一個巨大優點是,你將完全不需要變更原始的
getRandomArticle 函式:只要它回傳一個可被等候的 Promise 物件。
</p>
<p>
請注意,await 只能夠在非同步函式中使用,函式須以關鍵宇 async
標示。非同步函式的運作類似於產生器,可在本文中暫停執行直到 Promise
物件已確認狀態為止。若被等待的運算式並不是一個 Promise 物件,則會被轉換型態為 Promise
物件。
</p>
<p>
下面的程式碼使用我們最初的 getRandomArticle 函式,它使用 Promise
物件進行運作。接著它會透過一個名稱為renderView的非同步函式,傳回 HTML
結果更新頁面内容。請注意我們如何使用 try/catch 在被等候的 Promise
物件中進行錯誤理,就如我們在同步程式操作一樣。
</p>
<pre><code class="language-js">
async function read() {
try {
const model = await getRandomArticle()
const html = await renderView(model)
await setPageContents(html)
console.log('Successfully changed page!')
} catch (err) {
console.error(err)
}
}
read()
</code></pre>
<p>
非同步函式均會回傅一個 Promise 物件。萬一發生未捕捉的錯誤時,則回傅已拒絕的 Promise
物件。此外,回傳的物件也可解析為回傳值。非同步函式允許我們將這兩種回傳型態與一般以 Promise
物件為基礎的方法繼續搭配使用。下面範例說明如何將這兩種回傳型態結合運用。
</p>
<pre><code class="language-js">
</code></pre>
<pre><code class="language-js">
async function read() {
const model = await getRandomArticle()
const html = await renderView(model)
await setPageContents(html)
return 'Successfully changed page!'
}
read()
.then(message => console.log(message))
.catch(err => console.error(err))
</code></pre>
<p>
若要護 read 函式更具再利用性,我們可以將 html 結果回傳,並允許使用者利用Promise
或其他的非同步函式進行後續處理。這樣你的 read 函式功能就可以專注於擷取頁面的 HTML 內容。
</p>
<pre><code class="language-js">
async function read() {
const model = await getRandomArticle()
const html = await renderView(model)
return html
}
</code></pre>
<p>在下面範例中 ,我們運用單純的 Promise 物件將 HTML 輸出。</p>
<pre><code class="language-js">
read().then(html => console.log(html))
</code></pre>
<p>
使用非同步函式對後續的處理也較不會那麼困難。在下方的程式碼中,我們建立了一個 write
函式,作為後續的應用處理。
</p>
<pre><code class="language-js">
async function write() {
const html = await read()
console.log(html)
}
</code></pre>
<p>那同時進行的非同步流程會如何呢?</p>
<pre><code class="language-js">
</code></pre>
<h3 id="4-4-3">4-4-3 同時發生的非同步流程</h3>
<p>
在非同步程式碼的流程中,經常會同時地執行兩個或多個工作。利用非同步函式可以很容易的撰寫非同步程式,同時在程式上也將它們以一次進行一個非同步操作的方式撰寫。一個函式若有多個
await 運算式在其中,則在每一個await 運算執行時會暫停一次,直到 Promise
狀態確認,恢復運作並至下一個 await 運算之前一這和我們所看到的產生器與 yield
運算式的案例類似。
</p>
<pre><code class="language-js">
async function concurrent (){
const p1 = new Promise(resolve=>
setTimeout(resolve, 500,'fast')
)
const p2 = new Promise (resolve=>
setTimeout(resolve, 200,'faster')
)
const p3 = new Promise (resolve =>
setTimeout(resolve, 100,'fastest')
)
const r1 = await p1 // 運作會暫停,直到 p1 己確認狀態
const r2 = await p2
const r3 = await p3
}
</code></pre>
<p>
我們可以使用 Promise.all 修正此問題,透過建立一個我們可以 await 的 Promise
物件。和用此方式,我們的程式碼運作會暫停,直到清單中的每一個 Promise
物件都確認狀態後,它們就同時地完成解析。
</p>
<p>
下面的範例示範如何利用 await 等待三個不同的 Promise 物件並同時完成解析。假定 await
暫停了你的 async 函式,且 await Promise.all 運算式最終會將解析結果置於 results
結果陣列中。你可以利用解構賦值的方式將每一個結果值自陣列中取出。
</p>
<pre><code class="language-js">
async function concurrent (){
const p1 = new Promise(resolve=>
setTimeout(resolve, 500,'fast')
)
const p2 = new Promise(resolve=>
setTimeout(resolve, 200,'faster')
)
const p3 = new Promise(resolve=>
setTimeout(resolve, 100,'fastest')
)
const [r1, r2, r3] = await Promise.all([p1, p2, p3])
console.log(r1, r2, r3)
//'fast','faster','fastest'
}
</code></pre>
<p>我們可以使用 Promise.race 自較早實現的 Promise 物件中取得結果。</p>
<pre><code class="language-js">
async function race (){
const p1 = new Promise(resolve=>
setTimeout(resolve, 500,'fast')
)
const p2 = new Promise(resolve=>
setTimeout(resolve, 200,'faster')
)
const p3 = new Promise(resolve=>
setTimeout(resolve, 100,'fastest')
)
const result = await Promise.race([p1, p2, p3])
console.log(result)
// 'fastest'
}
</code></pre>
<h3 id="4-4-4">4-4-4 錯誤處理</h3>
<p>
在 async 函式中,錯誤會被無聲地抑制下來,就像是在內部的普通 Promise
物件;因為非同步函式被包裹於一個 Promise
物件中。若在你的非同步函式主體中發生了未被捕提的例外,或執行一個 await
運算式暫停運作的期間,將會拒絕 async 函式所回傳的 Promise 物件。
</p>
<p>
也就是說,除非我們在 await 運算式的周圍加入 try/catch
區塊,才能夠處理鐠誤。對非同步函式被包裹的程式部分,所發生的錯誤就會以 try/catch
的機制進行處理。
</p>
<p>
自然地,這可以視為是一項特點:某些無法用非同步回呼函式處理的事項,和某些以 Promise
物件可以處理的事項,你就可以使用
try/catch的錯誤處理機制。在這些情境下,非同步函式與產生器類似,可以運用 try/catch
錯誤處理機制,因為中止函式執行的動作可將非同步流程轉變為表面上同步的程式流程。
</p>
<p>
除此之外,藉由在函式回傳的 Promise 物件加入 .catch 子句,你可以自 async
函式外捕捉到發生的錯誤。結合 try/catch
子句是一個具有彈性的方法,但也會導致混淆並最終造成未捕捉處理的錯誤,除非所撰寫出來的非同步函式內容,在該環境下以
try/catch 的運作和 Promise 包裹的角度審視,均可讓人輕鬆理解。
</p>
<pre><code class="language-js">
read()
.then(html=> console.log(html))
.catch(err => console.error(err))
</code></pre>
<p>如你所見,還有一些方法可以捕捉到例外錯誤,並接著進行處理、記錄、或卸載。</p>
<h3 id="4-4-5">4-4-5 了解非同步函式的內部運作</h3>
<p>非同步函式在內部運用了產生器和 Promise 物件。假設我們有如下的非同步函式。</p>
<pre><code class="language-js">
async function example(a, b, c) {
// example 函式內容
}
</code></pre>
<p>
下面的程式範例說明如何將 example 函式宣告轉換為傳統的 function 敘述,它會將產生器傳入
spawn 輔助函式中的結果回傳。
</p>
<pre><code class="language-js">
function example(a, b, c) {
return spawn (function* () {
// example 函式內容
})
}
</code></pre>
<p>在產生器函式中,我們認為 yield 在功能語法上與 await 是相同的。</p>
<p>
在 spawn 函式中,Promise
物件被包裹於程式碼中,此程式碼會進入產生器函式(由使用者的程式要求)依序將值傳遞至產生器程式碼中
(async 函式的內容)。
</p>
<p>
以下的範例應該可以幫助你理解 sync/await 演算法如何運用產生器對一系列的 await
運算式進行迭代。在序列中的每個項目都會包裹於一個 Promise
物件中,並與序列的下一個項目鏈結。自產生器函式所回傳的 Promise 物件,在序列結束時或
Promise 物件被拒絕時,可以完成狀態確認。
</p>
<pre><code class="language-js">
function spawn (generator) {
// 將所有項目包裹於一個 Promise 物件中
return new Promise((resolve, reject) => {
const g = generator()
// 執行第一個步驟
step(() => g.next())
function step(nextFn){
const next = runNext (nextFn)
if (next. done) {
// 若成功結束,則將 Promise 物件完成解析
resolve(next.value)
return
}
// 若尚未結束,則將出產的 Promise 物件鏈結
// 並執行下一個步驟
Promise
.resolve(next.value)
.then (
value => step(() => g.next(value)) ,
err => step(() => g.throw(err))
)
}
function runNext (nextFn) {
try {
// 恢復產生器運行
return nextFn()
} catch (err) {
// 以失敗結束,則拒絕 Promise 物件
reject(err)
}
}
})
}
</code></pre>
<p>
參考下方的非同步函式,為了將結果輸出,我們以 Promise
物件為基礎接續處理的方式進行。下面讓我們依循程式碼先進行思考上的推演。
</p>
<pre><code class="language-js">
async function exercise() {
const r1 = await new Promise(resolve => setTimeout(resolve, 500, 'slowest'))
const r2 = await new Promise(resolve => setTimeout(resolve, 200, 'slow'))
return [r1, r2]
}
exercise().then(result => console.log(result))
// ['slowest', 'slow']
</code></pre>
<p>
首先,我們可以將函式轉換為以 spawn
函式為基礎的邏輯,我們將非同步函式的内容包裹於一個產生器中,傅遞給 spawn 函式,並將 await
以 yield 取代。
</p>
<pre><code class="language-js">
function exercise() {
return spawn(function* () {
const r1 = yield new Promise(resolve => setTimeout(resolve, 500,'slowest'))
const r2 = yield new Promise(resolve => setTimeout(resolve, 200, 'slow'))
return [r1, r2]
})
}
exercise().then(result=> console.log(result))
// ['slowest ','s1ow']
</code></pre>
<p>
當 spawn 函式被呼叫時,它會立刻建立一個產生器物件,並第一次執行 step
函式,如下面的程式範例。無論何時當程式執行觸及 yield 運算式,均會調用 step
函式;這和我們非同步函式中的 await 運算式效果相同。
</p>
<pre><code class="language-js">
function spawn (generator) {
// 將所有項目包裹於一個 Promise 物件中
return new Promise((resolve, reject) =>{
const g = generator()
// 執行第一個步驟
step(() => g.next ())
// -
})
}
</code></pre>
<p>
在 step 函式中發生的第一件事,就是呼叫 try/catch 區塊中的 nextFn
函式,這個動作會恢復產生器函式的運行。如果產生器函式發生錯誤,程式流程會進入到 catch
區塊,並且回傳的 Promise 物件為已拒絕狀態,不再進行後續的執行,如下範例所示。
</p>
<pre><code class="language-js">
function step(nextFn) {
const next = runNext(nextFx)
// -
}
function runNext (nextFn) {
try {
// 恢復產生器運行
return nextFn()
} catch (err) {
// 以失敗結束,則拒絕 Promise 物件
reject (err)
}
}
</code></pre>
<p>
回到我們的非同步函式,程式會恢復運行,直到觸及下方的敘述。此時不會發生錯誤,且非同步函式的運作會再度中止。
</p>
<pre><code class="language-js">
yield new Promise(resolve => setTimeout(resolve, 500,'slowest'))
</code></pre>
<p>
yield 運算所出產的結果會由 step 函式取得,以 next.value方式調用;當出現 next.done
時,產生器序列則已至尾端。在此案例中,我們在函式中取得的 Promise
物件可以精確地控制迭代的進行。當 next.done 為 false 時,我們不需要將非同步函式的 Promise
物件完成解析,而是將 next.value 包裹於一個已實現的 Promise
物件,這樣的方式是為了防止此時間點尚未取得一個 Promise 物件。
</p>
<p>
接著我們等待 Promise
物件轉換為已實現或已拒絕狀態。若物件為已實現,我們將已實現的值傳入至產生器函式中,也就是藉由擷取下一個產生器序列值時傳入
value 值;若物件為已拒絕,我們就使用 g.throw 方法,它會在產生器函式中引發錯誤,使得在
runNext 函式中拋出已拒絕的非同步函式的包裹器 Promise 物件。
</p>
<pre><code class="language-js">
function step (nextFn){
const next = runNext (nextFn)
if (next.done) {
// 若成功結束,則將 Promise 物件完成解析
resolve(next. value)
return
}
// 若尚未結束,則將出產的 Promise 物件鏈結,並執行下一個步驟
Promise
.resolve(next. value)
.then(
value => step(() => g.next (value)),
err => step (() => g. throw(err))
)
}
</code></pre>
<p>
產生器函式可呼叫 g.next() 時,代表函式已恢復運行,並藉由將值傳入至 g.next(value), yield
運算式會取得此 value 值並進行計算結果。在這個案例裡,此 value 值是最初所出產的 Promise
物件的已實現結果值,即為'slowest'。
</p>
<pre><code class="language-js">
// 回到產生器函式,我們將 'slowest' 指派給 r1。
const r1 = yield new Promise(resolve =>setTimeout(resolve, 500,'slowest'))
</code></pre>
<p>
接著,程式開始執行,直到第二個 yield 運算式;它又會再次將非同步函式暫停,並將一個新的
Promise 物件傳遞至 spawn 迭代器。
</p>
<pre><code class="language-js">
yield new Promise(resolve=> setTimeout(resolve, 200, 'slow'))
</code></pre>
<p>
這次,相同的程序又重複的執行: next.done 為false,因為產生器函式尚未結束。我們將 Promise
物件包裹於另一個 Promise 物件中,一旦 Promise 物件狀態確認且具有
's1ow'值時,就可以恢復產生器函式的執行。
</p>
<pre><code class="language-js">
// 接下來會執行到產生器函式的回傳敘述。再一次,產生器函式的運行會終止,並回傅給迭代。
return [r1, r2]
// 此處, next 方法會回傳下方的物件。
{
value: ['slowest','slow'],
done: true
}
// 立即地,迭代器會檢查 next.done 是否為 true,且解析非同步函式的結果為['slowest','slow']
if (next. done){
// 成功結束,解析 Promise 物件值
resolve(next.value)
return
}
// 至此, exercise 所回傅的 Promise 物件確認為已質現, 並將最終的特果印出。
exercise().then(result => console.log(result))
// ['slowest,'slow']
</code></pre>
<p>
接著,想要在迭代產生器函式的過程
,可以來回順暢地傳遞值時,利用非同步函式會是較合理的方式。有些語法糖會隱藏產生器函式的使用,如:spawn
函式被運用來迭代 yield 運算的結果序列,yield 以 await 替 換。
</p>
<p>
我們從 Promise 的觀點來思考非同步函式。參考下方的範例,此處有一個非同步函式需等待一個
Promise
物件,而此物件來自於一個函式呼叫的回傳結果;接著等待每位使用者對應的函式結果。該如何將解釋這樣的敘述轉換為以
Promise 物件為基礎的方式進行呢?
</p>
<pre><code class="language-js">
async function getUserProfiles(){
const users = await findAllUsers()
const models = await Promise.all(users.map(toUserModel))
const profiles = models.map(model => model.profile)
return profiles
}
</code></pre>
<p>
下面的程式碼功能與 getUserProfiles 的非同步函式相同。在大部分的狀況下,我們會將 await
敘述變更為鏈結的 Promise 物件,同時將非同步函式中的變數宣告部分,移入至每一個 Promise
物件的回應中。在此範例中,假定非同步函式均會回傳一個 Promise 物件,但是我們心中須記得將
Promise.resolve(result) 的回傳值,指定給所有需要轉換為 Promise 物件的非同步函式。
</p>
<pre><code class="language-js">
function getUserProfiles(){
const userPromise = findAllUsers()
const modelPromise = userPromise.then(users =>
Promise.all(users.map(toUserModel))
)
const profilePromise = modelPromise.then(models =>
models.map(model => model.profile)
)
return profilePromise
}
</code></pre>
<p>
注意,非同步函式是在產生器和 Promise
物件之上的語法糖。學習這每一個建構元件的運作及使用,才能夠進一步瞭解後續在非同步程式碼流程中的搭配運用•
</p>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>