-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path1-2-5.html
630 lines (622 loc) · 23.6 KB
/
1-2-5.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
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
<!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 id="1-2-5">1-2-5 實作非同步流程</h2>
<h3>需求.1 製作一個非同步的方法</h3>
<p>
行動頁面上的元素 target(document.querySelectorAll(#man)[0]),先從原點出發,向左移動
20px,再向上移動50px,最後再向左移動 30px,請把運動路徑動畫實現出來。
</p>
<h4>思路</h4>
<p>將移動的過程封裝成一個 walk 函數,該函數要接受以下 3 個參數。</p>
<ul>
<li>direction:字串,表示移動方向,這裡簡化為 "left"、"top" 兩種列舉。</li>
<li>distance:整數,可正可負。</li>
<li>callback:執行動作後回呼。</li>
</ul>
<p>direction 表示移動方向, distance 表示移動距離。</p>
<p>透過 distance 的正負值,可以實現4個方向的移動。</p>
<p>
每一個任務都是相互聯繫的:目前任務結束後,將馬上進入下一個流程,如何將這些流串聯起來呢?這裡採用最簡單的
callback 來明確指示下一個任務。
</p>
<h4>方案.1 回呼</h4>
<pre><code class="language-js">
const target = (document.querySelectorAll('#man')[0].target.style.cssText = `
position: absolute;
left: 0px;
top: 0px
`)
const walk = (direction, distance, callback) => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish =
(direction == 'left' && currentLeft === -distance) ||
(direction === 'top' && currentTop === -distance)
if (shouldFinish) {
//任務執行結束,執行下一個回呼
callback && callback()
} else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
} else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
walk(direction, distance, callback)
}
}, 20)
}
walk('left', 20, () => {
walk('top', 50, () => {
walk('left', 30, Function.prototype)
})
})
</code></pre>
<p>
關於以上程式,有兩點需要注意。第一點,為了簡化問題,將目標元素的定位進行了如下所示的初始化設定,且不再考慮邊界情況(如移出螢幕外等)。
</p>
<pre><code class="language-js">
position: absolute;
left: 0px;
top: 0px;
</code></pre>
<p>
第二點,為了能夠展現出動畫,我們將 walk 函數的執行邏輯包裹在 20ms
的計時器中,每次執行一像素的運動都會有一個停留定格。
</p>
<p>
這樣的實現是完全針對過程的,只是程式比較「醜」,只需體會使用回呼來解決非同步任務的處理方案即可。另外,如下所示的回呼巢狀結構很不優雅,有幾次位移任務就會巢狀結構幾層,是名副其實的回呼地獄。
</p>
<pre><code class="language-js">
walk('left',20,()=>{
walk('top',50,()=>{
walk('left', 30, Function.prototype)
})
})
</code></pre>
<h4>方案.2 Promise</h4>
<p>來看一下如何用 Promise 解決問題,程式如下。</p>
<pre><code class="language-js">
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
position: absolute;
left: 0px;
top: 0px
`
const walk = (direction, distance) =>
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish =
(direction === 'left' && currentLeft === -distance) ||
(direction === 'top' && currentIop == -distance)
if (shouldFinish) {
// 任務執行結束
resolve()
} else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
} else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
walk('left', 20)
.then(() => walk('top', 50))
.then(() => walk('left', 30))
</code></pre>
<p>以上需要注意以下幾點:</p>
<ul>
<li>
walk 函數不再巢狀結構呼叫,不再執行 callback ,而是整體傳回一個 Promise
,以便控制和執行後續任務。
</li>
<li>設定 innerWalk 對每個像素進行遞迴呼叫。</li>
<li>在目前任務結東時 (shouldFinish 為 true),對目前 Promise 進行決議。</li>
</ul>
<p>比較後發現 Promise 還是簡單易讀。</p>
<h4>方案.3 Generator</h4>
<p>
ES Next 中的
Generator(產生器)其實並不是為解決非同步問題而生的,但是它又天生非常適合解決非同步問題。用Generator
方案解決非同步問題的範例如下。
</p>
<pre><code class="language-js">
const target = document.querySelectorA11('#man')[0]
target.style.cssText = `position: absolute;
left: 0px;
top: 0px
`
const walk = (direction, distance) =>
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish =
(direction === 'left' && currentLeft === -distance) ||
(direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任務執行結束
resolve()
} else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
} else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
function* taskGenerator() {
yield walk('left', 20)
yield walk('top', 50)
yield walk('left', 30)
}
const gen = taskGenerator()
</code></pre>
<p>
在以上程式中,我們定義了一個 taskGenerator 產生器函數,並產生實體出 gen
。在此基礎上,我們可以手動執行 gen.next() :使目標物體向左偏移 20px ;再次手動執行
gen.nex() 會使目標物體向上偏移 50 px;第三次手動執行 gen.next() 會使目標物體向左偏移
30px。
</p>
<p>
整個過程掌控感十足,唯一的不便之處就是需要我們反覆手動執行gen.next()
對於這個問題,社區早就列出了解決方案,kj大神的 co 庫能夠自動包裹 Generator
並執行,其原始程式並不複雜,這裡推薦大家閱讀一下。但是在新時代裡,作為 Generator
的語法糖,async/await 也許將是更優雅的終極解決方案。
</p>
<h4>方案.4 async/await</h4>
<p>將方案.3 改成 async/await 範例如下</p>
<pre><code class="language-js">
const target = document.querySelectorAll('#man')[0]
target.style.cssText = `
position: absolute;
left: 0px;
top: 0px`
const walk = (direction, distance) =>
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target, style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish =
(direction === 'left' && currentLeft === -distance) ||
(direction === 'top' && currentTop === -distance)
if (shouldFinish) {
//任務執行結束
resolve()
} else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
} else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
const task = async function () {
await walk('left', 20)
await walk('top', 50)
await walk('left', 30)
}
</code></pre>
<p>經過改造,使用 async/await方案只需直接執行 task 函數即可使物體做出預期中的運動軌跡。</p>
<p>
透過比較 Generator 和 async/await 這兩種方案,應該意識到,async/await 就是 Generator
的語法糖,能夠自動執行產生器函數,且可以更加方便地實現非同步流程。
</p>
<h3>需求.2 紅綠燈任務控制</h3>
<p>紅燈3s亮一次,綠燈1s亮一次,黃燈2s亮一次,如何讓3個燈不斷交替重複地亮呢?</p>
<p>這道題目如果用 Promise 方案實現的話,程式如下。</p>
<pre><code class="language-js">
const task = (timer, light) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const step = ()=>{
task(3000, 'red')
.then(()=> task(1000,'green'))
.then(() => task(2000,'yellow'))
.then(step)
}
step ()
</code></pre>
<pre><code class="language-js">
const taskRunner = async ()=> {
await task(3000, 'red')
await task(1000,'green')
await task(2000,'yellow')
taskRunner()
}
taskRunner()
</code></pre>
<p>該用什麼方案一目了然。</p>
<p>
可見,熟悉 Promise 是基礎,是了解 async/await 的前提,學習 async/await
就是在學習「最先進的生產力」。當然要再次重申: async/await 是語法糖,學習 Promise
是消化這顆「糖」的前提。
</p>
<h3>需求.3 請求圖片進行預先載入</h3>
<p>
假設 urlIds
陣列預先就存在,陣列中的每一項都可以按照規則連接成一個完整的圖片位址,那麼請根據這個陣列,依次請求圖片進行預先載入。
</p>
<p>這個問題解決起來比較簡單,我們先實現一個請求圖片的方法,程式如下。</p>
<pre><code class="language-js">
const loadImg = (urlId) => {
const url = `https://www.image.com/${urlId}`
return new Promise((resolve, reject) => {
const img = new Image()
img.onerror = function () {
reject(urlId)
}
img.onload = function () {
resolve(urlId)
}
img.src = url
})
}
</code></pre>
<p>
該方法進行過 Promise 化(promisify)的處理,在圖片成功載入時執行 resolve,載入失敗時執行
reject。
</p>
<p>根據圖片 urlId,依次請求圖片,程式如下。</p>
<pre><code class="language-js">
const urlIds = [1, 2, 3, 4, 5]
urlIds.reduce((prevPromise, UrLId) => {
return prevPromise.then(() => loadImg(urlId))
}, Promise.resolve())
</code></pre>
<p>上面使用了陣列的 reduce 方法,當然還可以使用針對過程的方法來實現,程式如下。</p>
<pre><code class="language-js">
const loadImgOneByOne = (index) => {
const length = urlIds.length
loadImg(urlIds[index]).then(() => {
if (index === length - 1) {
return
} else {
loadImgOneByOne(++index)
}
})
}
loadImgOneByOne(0)
</code></pre>
<p>另外,還可以採用 async/await 來實現,程式如下。</p>
<pre><code class="language-js">
const loadImgOneByOne = async () => {
for (i of urlIds) {
await loadImg(urlIds[i])
}
}
loadImgOneByOne()
</code></pre>
<p>上述程式的請求都是依次執行的,只有成功載入完第一張圖片,才能繼續載入下一張圖片。</p>
<p>如果想要提高效率,將所有圖片的請求一次性發出,該如何做呢?請看以下程式。</p>
<pre><code class="language-js">
const urlIds = [1, 2, 3, 4, 5]
const promiseArray = urlIds.map((urlId) => loadImg(urlId))
Promise.all(promiseArray)
.then(() => {
console.log('finish load all')
})
.catch(() => {
console.log('promise all catch')
})
</code></pre>
<p>
繼續提出需求:我們希望控制最大平行處理數為3,最多一起發出3個請求,剩下2個一起發出,又該怎麼做呢?這就需要實現一個loadByLimit
方法,實現時可以考慮使用 Promise.race方法,程式如下
</p>
<pre><code class="language-js">
const loadByLimit = (urlIds, loadImg, limit) => {
const urlIdsCopy = [...urlIds]
if (urlIdsCopy.length <= limit) {
// 如果陣列長度小於最大平行處理數,則直接發出全部請求
const promiseArray = urlIds.map((urlId = loadImg(urlId)))
return Promise.all(promiseArray)
}
// 注意,splice方法會改變 urlIdsCopy 陣列
const promiseArray = urlIdsCopy.splice(0, limit).map((urlId) => loadImg(urlId))
urlIdsCopy
.reduce(
(prevPromise, urlId) =>
prevPromise
.then(() => Promise.race(promiseArray))
.catch((error) => {
console.log(error)
})
.then((resolvedId) => {
//將resolvedId從 promiseArray陣列中刪除
// 這裡用於刪除操作的只是虛擬程式碼,實際刪除情況要看後端 API 傳回的結果
let resolvedIdPosition = promiseArray.findIndex((id) => resolvedId === id)
promiseArray.splice(resolvedIdPosition, 1)
promiseArray.push(loadImg(urlId))
}),
Promise.resolve()
)
.then(() => Promise.all(promiseArray))
}
</code></pre>
<h3>setTimeout 解析</h3>
<p>觀察以下程式。</p>
<pre><code class="language-js">
setTimeout(() => {
console.log('setTimeout block')
}, 100)
while (true) {}
console.log('end here')
</code></pre>
<p>
行以上程式符不會輸出任何結果,因為 while 巡迴回不斷執行,佔用主執行緒。
但是,執行以下程式會列印出 end here。
</p>
<pre><code class="language-js">
setTimeout(() => {
while (true) {}
},0)
console.log('end here')
</code></pre>
<p>這段程式執行後,再執行任何敘述都不會獲得回應。</p>
<p>JavaScript 中的任務分為同步任務和非同步任務。</p>
<ul>
<li>
同步任務:目前主執行緒將要消化執行的任務,這些任務一起形成執行基疊 (execution context
stack)。
</li>
<li>
非同步任務:不進入主執行緒,而是進入任務佇列(task queue),即不會馬上進行的任務。
</li>
</ul>
<p>
當同步任務全都被消化,主執行緒空閒時,即上面提到的執行堆疊為空時!
系統將執行任務佇列中的任務,即非同步任務。
</p>
<p>
JS為單行執行的程式語言,操作中將需要耗時的任務丟進任務佇列中,這樣就不會阻礙主執行緒的執行。
</p>
<p>如果將相關程式修改為如下所示的樣子</p>
<pre><code class="language-js">
const t1 = new Date()
setTimeout(() => {
const t3 = new Date()
console.log('setTimeout block')
console.log('t3-t1=' t3 - t1)
}, 100)
let t2 = new Date()
while (t2 - t1 < 200) {
t2 = new Date()
}
console.log('end here')
// 則輸出結果如下。
// end here setTimeout block
// t3 - t1 = 200
</code></pre>
<p>
雖然setTimeout 計時器的定時為100ms,但是由於同步任務中的while 循環將執行
200ms,因此計時完成後仍然會先執行主執行緒中的同步任務,只有當同步任務全部執行完畢,輸出
end here
時,才會開始執行任務佇列中的任務。此時,t3和t1的時間差為200ms,而非計時器設定的100ms。
</p>
<p>關於 setTimeout 最容易被忽視的其實是一個非常小的細節。請看以下程式。</p>
<pre><code class="language-js">
setTimeout(() => {
console.log('here 100')
}, 100)
setTimeout(() => {
console.log('here 2')
}, 0)
</code></pre>
<p>但是,如果將程式修改成下面的樣子,情況又會如何呢?</p>
<pre><code class="language-js">
setTimeout(() => {
console.log('here 1')
}, 1)
setTimeout(() => {
console.log('here 2')
}, 0)
</code></pre>
<p>
按道理來說,執行這段程式時也應該是第二個 setTimeout 將更快被觸發,先 輸出 here
2,再輸出here 1・但是,在 Chrome 瀏覽器中執行的結果相反,事 實上,這兩個 setTimeout
誰先進入任務佇列,誰就會先執行,並不會嚴格按 照 1ms 和 0ms 進行區分。
</p>
<p>
表面上看,1ms 和0ms 的延遲是完全相等的,有點類似「最小延時間」這個概念。直觀上看,最小延
時間是1ms,在1ms以內的定時都以最小延遲時間處理。此時,誰在程式順序上靠前,誰就會在主執行緒空閒時被優先執行。
</p>
<p>
可以參考一下 MDN上列出的最小延遲時間——4ms,另外,setTimeout 也有「最大延
時間」的概念。這都依賴於標準的制定和瀏覽器引擎的實現
</p>
<h3>巨任務和微任務</h3>
<p>在此之前先看一段程式</p>
<pre><code class="language-js">
console.log('start here')
new Promise((resolve, reject) => {
console.log('first promise constructor')
resolve()
})
.then(() => {
console.log('first promise then')
return new Promise((resolve, reject) => {
console.log('second promise')
resolve()
}).then(() => {
console.log('second promise then')
})
})
.then(() => {
console.log('another first promise then')
})
console.log('end here')
</code></pre>
<ul>
<li>首先會輸出 start here,這一點是沒有問題的。</li>
<li>
接著是一個Promise建構函數,執行同步程式,輸出 first promise constructor,同時將第一處
Promise 的 then 方法完成處理邏輯放入任務佇列。
</li>
<li>繼續執行同步程式,輸出 end here。</li>
<li>
同步程式全部執行完畢,然後執行任務佇列中的邏輯,輸出 first promise then 及 second
promise
</li>
<li>
當在 then 方法中傳回一個 Promise 時(第9行),第一個 Promise 的第二個 then
方法對應的完成處理函數(第17行)便會置於傳回的這個新 Promise的then方法(第13行)之後。
</li>
<li>
此時將傳回的這個新 Promise 的
then方法放到任務佇列中,由於主執行緒中沒有其他任務,因此會轉而執行第二個 then 任務,輸出
second promise then
</li>
<li>最後輸出 another first promise then</li>
</ul>
<p>
任務佇列中的非同步任務其實又分為巨任務(macrotask)與微任務(microtask),也就是說巨任務和微任務雖然都是非同步任務,都在任務份列中,但是它們在兩個不同的佇列中。
那麼,如何區分巨任務和微任務呢?
</p>
<p>一般情況下,巨任務包含以下內容。</p>
<ul>
<li>setTimeout</li>
<li>setInterval</li>
<li>I/O</li>
<li>事件</li>
<li>postMessage</li>
<li>setImmediate (Node.js 中的特性,瀏覽器端已經廢棄該 API)</li>
<li>requestAnimationFrame</li>
<li>UI 繪製</li>
</ul>
<p>微任務包含以下內容。</p>
<ul>
<li>Promise.then</li>
<li>MutationObserver</li>
<li>process.nextTick (Node.js)</li>
</ul>
<p>那麼,當程式中同時存在巨任務和微任務時,誰的優先順序更高,請看以下程式。</p>
<pre><code class="language-js">
console.log('start here')
const foo = () =>
new Promise((resolve, reject) => {
console.log('first promise constructor')
let promise1 = new Promise((resolve, reject) => {
console.log('second promise constructor')
setTimeout(() => {
console.log('setTimeout here')
resolve()
}, 0)
resolve('promise1')
})
resolve('promise0')
promise1.then((arg) => {
console.log(arg)
})
})
foo().then((arg) => {
console.log(arg)
})
console.log('end here')
</code></pre>
<p>首先輸出 start here,執行 foo 函數,同步輸出 first promise constructor。</p>
<p>
繼續執行foo函數,遇見 promise1,執行 promise1 建構函數,同步輸出second promise constructor
及 end here。同時,按照順序依次執行以下操作,setTimeout
回呼進入任務佇列(巨任務),promise1
的完成處理函數(第18行)進入任務佇列(微任務),第一個(匿名)promise
的完成處理函數(第23行)進入任務佇列(微任務)。 雖然 setTimeout
回呼率先進入任務佇列,但是引擎會優先執行微任務,按照微任務的順序先輸出 promise1
的結果,再輸出 promise0 的結果(即第一個匿名 promise 的結果)。
</p>
<p>
此時,所有微任務都處理完畢,開始執行巨任務,輸出 setTimeout 回呼內容 setTimeout here。
</p>
<p>
由上分析得知,每次主執行緒執行堆疊為空的時候,引擎都會優先處理做任務佇列,處理完微任務佇列中的所有任務,再處理巨任務,如同以下程式及
輸出結果一般。
</p>
<pre><code class="language-js">
console.log('start here')
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve, reject) => {
resolve('promise result')
}).then((value) => {
console.log(value)
})
console.log('end here')
// 輸出結果
// start here end here
// promise result
// setTimeout
</code></pre>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
<script></script>
</html>