-
Notifications
You must be signed in to change notification settings - Fork 0
/
1-6-2.html
1124 lines (1097 loc) · 41.6 KB
/
1-6-2.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
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!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-6-2">1-6-2 單向數據流架構 React 解析</h2>
<p>
React 開發者可能對各種生命週期瞭若指掌,也熟悉「React 虛擬 DOM diff
演算法」、「單向資料流程」。但這並不夠深入。以下來介紹一些更加深入核心的原理。
</p>
<h3>React 專屬語法 JSX</h3>
<p>從直觀上看 JSX 是將 HTML 直接嵌入了 JS (JavaScript)中。</p>
<p>如果我們需要用條件判斷是否顯示元件有多種寫法。</p>
<p>
假設收到這樣一個很常見的需求:繪製一個清單,但是需要滿足清單為空陣列時,顯示空文案"Sorry,the
list is empty"。清單中的資料可能需要透過網路取得,存在列表中沒有初值為 null 的情況。
</p>
<p>透過 JSX 實現這種條件繪製的最簡潔的方法就是使用三元運算子</p>
<pre><code class="language-js">
const list = ({list}) => {
const isNull = !list
const isEmpty = !isNull && !list.length
return (<div> {
isNull ? null:( isEmpty:<p>Sorry,the list is empty </p>
:<div>{ list.map(item => <ListItem item={item} />) }
</div>
)
}
</div>
)
}
</code></pre>
<p>
由於之後還要加上了出現錯誤時和正在載入時的邏輯,所以三元運算子萬惡的巢狀結構地獄可能就要出現了。解決思路通常用的方法是抽離出
render function ,如下:
</p>
<pre><code class="language-js">
const getListContent = (isLoading, list, error) => {
console.log(list)
console.log(isLoading)
console.log(error)
//...
return
//...
}
const list = ({isLoading, list, error}) => {
return (<div>{
getListContent (isLoading, list, error)
} </div>)
}
</code></pre>
<p>甚至可以使用 IIFE,範例如下</p>
<pre><code class="language-js">
const list = ({isLoading, 11st, error})=> {
return (<div>
{
(()=>{
console.log(list)
console.log(isLoading)
console.log(error)
if(error) {
return <span>Something is wrong!</span>
}
if (!error && isLoading) {
return <span>Loading...</span>
}
if (!error && lisLoading && !list. length) {
return <p>Sorry, the list is empty </p>
}
if (!error && !isLoading && list.length > 0){
return <div>{
list.map(item => <ListItem item={item} />)
}
</div>
}
})()
}
</div>
)
}
</code></pre>
<p>這樣一來就可以使用 console.log 進行簡單偵錯了,也可以使用 if.else 實現條件繪製。</p>
<p>
再回到根本問題:為什麼不能直接在 JSX 中使用 if.else
實現條件繪製,而只能借用函數運輯實現呢?實際上,我們都知道 JSX 會被編譯為 React.
createElement。直白地說,React createElement 的底層邏輯是無法執行 JavaScript
程式的,只能繪製一個結果。因此,JSX 中除了 JavaScript 運算式,不能直接寫 JavaScript
語法。準確來講,JSX 只是函數呼叫和運算式的語法糖。
</p>
<p>
雖然 JSX 只是函數呼叫和運算式的語法糖,但是JSX 仍然具有強大的功能且能夠使用靈活。React
元件重複使用最流行的方式都是以 JSX 能力為基礎的, 舉例來説,HOC(Higher Order
Component,高階元件)和 render prop 模式, render prop 模式的實現程式如下
</p>
<pre><code class="language-js">
class WindowWidth extends React.Component {
constructor () {
super()
this.state = {
width: 0
}
}
componentDidMount () {
this.setState (
{width: window.innerWidth},
window.addEventListener('resize',({target}) =>{
this.setState({
width: target.innerWidth
})
})
)
}
render (){
return this.props.children(this.state.width)
}
}
<WindowWidth>
{width => (width => 800 ? <div>show</div> : null)}
<WindowWidth>
</code></pre>
<p>甚至還可以讓 JSX 具有 Vue template 的能力,範例程式如下。</p>
<pre><code class="language-js">
render (){
const visible = true
return (
<div>
<div v-if={visible}>
content
</div>
</div>
)
}
render () {
const list = [1, 2, 3, 4]
return (
<div>
<div v-for={item in list}>
{item}
</div>
</div>
)
}
</code></pre>
<p>JSX 需要在編譯後執行,在編譯過 中借助 AST(抽象語法樹)對 v-if、v-for進行處理即可。</p>
<p>這裡註記一下:操作AST 是另外一門高階技術。需要額外開一章說明。</p>
<h3>非同步的 this.setState</h3>
<p>
絕大多數 React 開發者都知道 this.setState
是非同步執行的,但這個結論是錯誤的!那麼,this.setState 到底是非同步執行還是同步執行的?
</p>
<p>
this.setSate 這個 API 的官方描述為: setState() does not always immediately update the
component. It may batch or defer the update until later. This makes reading this state
right after calling setState() a potential pitfall.
</p>
<p>
意思是說,setState() 方法並不總是能夠立刻更新元件,它可能會延遲更新,這樣在透過 this.state
讀取內容時,就有可能取得不到最新的狀態值。
</p>
<p>
既然用詞是 may,那麼說明 this.setState
一定不總是非同步執行的,也不總是同步執行的。所謂的延遲更新並不是針對所有情況的。
</p>
<p>
實際上,在React 控制之內的事件處理過程中,setState 不會同步更新 this. state ;而對於 React
控制之外的情況,setState 會同步更新 this.state。
</p>
<p>什麼是 React 控制內外呢?舉個實例,先來看以下程式。</p>
<pre><code class="language-js">
onClick(){
this.setState({
count: this.state.count + 1
})
}
componentDidMount(){
document.querySelectorAll('#btn-raw')
addEventListener('click', this.onclick)
}
render(){
return(
<React.Fragment>
<button id="btn-raw">
click out React
</button>
<button onClick={this.onClick}>
click in React
</button>
</React.Fragment>
)
}
</code></pre>
<p>
在 為 btn-raw 的 button 上綁定的事件,是在 componentDidMount 方法中添過 addEventListener
完成更新的,這是脱離於 React 事件之外的事件,因此它是同步更新的。相反,在程式中第二個
button 上綁定的事件處理函數對應的 setState
是非同步更新的。這樣的設計並不難了解,透過延遲更新,可以獲得更好的效能。
</p>
<h4>this.setState Promise 化</h4>
<p>
官方提供了 this.setState 這種處理非同步更新的方法。其中的方法就是使 setState
接收第二個參數,並作為狀態更新後的回呼,但這無疑又帶來了我們熟悉的 callback
hell(回呼地獄)問題。
</p>
<p>
例如有這樣一個場景,我們在開發一個 table,這個 table 類似Excel
的形式,使用者敲下確認鍵時,需要將游標移動到下一行,這是一個 setState
操作,然後馬上進行聚焦,這又是一個 setState
操作。如果目前行就是最後一行,那麼使用者敲下確認鍵時,需要先建立一個新行,這是第一個setState
操作,同時將游標移動到新的最後一行,這是第二個 setState
操作;然後在這個新行中進行聚焦,這是第三個 setState 操作。這些 setState 操作依賴於前一個
setState 操作的完成。
</p>
<p>
面對這種場景,如果我們不想讓回呼地獄問題出現,那麼常見的處理方式就是利用生命週期方法,在
componentDidUpdate 中進行相關操作,第一個 setState 操作完成後,在其觸發的
componentDidUpdate 中進行第二個 setState 操作,依此類推
</p>
<p>
但是這樣存在的問題也很明顯:邏輯過於分散。生命週期方法中有很多很難維護的「莫名其妙的操作」會出現「針對生命週期程式設計」的情況
</p>
<p>
回到剛才的問題,解決回呼地獄問題其實是前端工程師的拿手好戲,最直接的方案就是將 setState
Promise 化,程式如下。
</p>
<pre><code class="language-js">
const setStatePromise(me, state)=>{
new Promise (resolve => {
me.setState(state,()=>{
resolve()
})
})
}
</code></pre>
<p>這只是系統更新的做法,如果使用修改 React 原始程式的做法,也不困難,如下圖所示。</p>
<img src="./assets/image/1-6-2/1.jpeg" alt="" />
<h3>原生事件和 React 合成事件</h3>
<p>
React 中的事件機制並不是原生的那一套,事件沒有綁定在原生DOM上,大多數事件都綁定在 document
上(除了少數不會上浮到 document 的事件,如 video等)。
</p>
<p>同時,React 觸發的事件也是對原生事件的包裝,並不是原生event物件。</p>
<p>
合成事件物件將被重用,因此在呼叫事件回呼函數後,合成事件物件上的所有屬性都將被廢棄。這樣做可以大幅節省記憶體,而不會頻繁地建立和銷毀事件物件。
</p>
<p>這樣的事件系統設計,會使效能有所提升,但同時也會引發幾個潛在現象。</p>
<h4>現象1:非同步存取事件物件</h4>
<p>我們不能以非同步的方式存取合成事件物件。以下程式所展示的是一個典型的錯誤範例。</p>
<pre><code class="language-js">
function handleClick (e){
console.log(e)
setTimeout(()=>{
console.log(e)
},0)
}
</code></pre>
<p>
在以上程式中,第二個 console.log 總會輸出 undefined。為此,React
也為我們準備了持久化合成事件的方法,如下。
</p>
<pre><code class="language-js">
function handleClick(e){
console.log(e)
e.persist() // 持久化
setTimeout(()=>{
console.log(e)
},0)
}
</code></pre>
<h4>現象2:阻止原生事件上浮</h4>
<p>
在 React 中直接使用 e.stopPropagation 不能阻止原生事件上浮,因為事件早已經上浮到了
document 上,React 在事件上浮到 document 上時才能夠處理事件。以下程式證明了 React
中是無法使用 e.stopPropagation 來阻止事件上浮的。
</p>
<pre><code class="language-js">
componentDidMount(){
document.addEventListener('click',()=>{
console.log('document click')
})
}
handleClick=e=>{
console.log('div click')
e.stopPropagation()
}
render(){
return(
<div onclick={this.handleClick}>
click
</div>
)
}
</code></pre>
<p>
以上程式執行後首先會列印出 div click,然後列印出 document click。e.stopPropagation
是沒有用的。
</p>
<p>
但是,React 的合成事件為使用原生事件留了一個口,透過合成事件上的 nativeEvent
屬性可以存取原生事件。原生事件上的stopImmediatePropagation 方法除了能做到像
stopPropagation
一樣阻止事件向父級上浮,還能阻止目前元素剩餘的、同類型事件的執行(第一個click
事件觸發時,呼叫e.stopImmediatePropagation 能夠阻止目前元素的第二個 click
事件觸發)。因此,執行以下程式只會列印出 div click。
</p>
<pre><code class="language-js">
componentDidMount (){
document.addEventListener('click',()=>{
console.log('document click')
})
}
handleClick = e => {
console.log('div click')
e.nativeEvent.stopImmediatePropagation ()
}
render(){
return(
<div onclick={this.handleClick}>
click
</div>
)
}
</code></pre>
<h3>element diff 演算法的細節</h3>
<p>
React的三個假設在比較 element 時存在缺陷,所以需要開發者為每一個 element提供 key,以便
React可以準確地發現新舊集合節點中的相同節點,對
於相同節點無須進行節點刪除和建立,只需要將舊集合中節點的位置進行移動,更新為新集合中節點的位置,如
圖2 所示。
</p>
<p>
元件的排列順序由1234變為2143,此時 React 列出的 diff
結果為2,對4不做任何操作,對1和3執行移動操作即可。
</p>
<p>
也就是說,如果元素在舊集合中的位置與在新集合中的位置相比更靠後的話,就不需要移動。當然,這種
diff 聽上去並非完美無缺。
</p>
<p>圖2</p>
<img src="./assets/image/1-6-2/2.jpeg" alt="" />
<p>圖3</p>
<img src="./assets/image/1-6-2/3.jpeg" alt="" />
<p>
對於圖3
所示的情況,實際上,只需要對4執行移動操作,然而由於4在舊集合中的位置是最靠後的,因此其他節點都要移動到4後面。
</p>
<p>
這種做法無疑是很愚蠢的,效能也會比較差。針對這種情況,官方建議,在開發過程中,儘量減少類似將最後一個節點移動到列表表頭的操作。
</p>
<p>
實際上,很多類別 React 類別庫(Inferno.js、Preact.js)都有了更優的 element diff 移動策略。
</p>
<h3>不是每次加上 Key 都能效能最佳化</h3>
<p>
剛才提到,React 在進行 element 差異比較時,由於key 的存在,React
可以準確地判斷出該節點在新集合中是否存在,這相當大地加強了 element 差異比較的效率。
</p>
<p>
但是接下來的案例卻不一定。現在有個場景,將集合[1,2,3.4]繪製成4組數字(僅是數字這麼簡單)
</p>
<pre><code class="language-html">
<div id="1">1</div>
<div id="2">2</div>
<div id="3">3</div>
<div id="4">4</div>
</code></pre>
<p>
如果要將它變為
[2,1.4,5],也就是刪除3,增加5,那麼按照之前的演算法,需要把1放到2後面,刪除3,再新增5。整個操作移動了一次
DOM 節點,刪除和新增節點加起來一共2處。
</p>
<p>
由於DOM 節點的移動操作負擔是比較昂貴的,因此對於這種簡單的 node text
更改,不需要進行類似的 element diff過程,只需要更改 dom.textContent 即可。dom.textContent
中的內容如下所示。
</p>
<pre><code class="language-js">
const startTime = performance.now()
$('#1').textContent = 2
$('#2').textContent = 1
$('#3').textContent = 4
$('#4').textContent = 5
console.log('time consumed:' performance.now() - startTime)
</code></pre>
<p>這麼看,也許不加key的效能要比加上key 的效能更高。</p>
<h2>React :元件設計</h2>
<p>首先來看第一個概念:單一職責。</p>
<h3>單一職責</h3>
<p>
將全部元件都拆散,使它們只具備單一職責並沒有必要,反而增加了撰寫的繁瑣程度。如果一個功能集合有可能發生變化,那麼就需要大幅地確保單一職責。
</p>
<p>
單一職責帶來的最大好處就是在修改元件時能夠做到對全域進行掌控,不必擔心對其他元件造成影響。舉個實例,我們的元件需要透過網路請求取得資料並展示資料內容,這樣一來潛在的功能集合改變就有以下幾種。
</p>
<ul>
<li>請求API 位址發生變化。</li>
<li>請求傳回資料格式發生變化。</li>
<li>開發者想更換網路請求協力廠商函數庫,例如:將jQuery.ajax 改成 axios。</li>
<li>更改請求資料邏輯。</li>
</ul>
<p>再看一個實例,如果需要一個 Table 元件,並繪製一個 List,那潛在更改可能如下。</p>
<ul>
<li>限制一次性繪製的 item 個數(只繪製前 10個,對剩下的進行 lazy-load)。</li>
<li>當資料清單為空時顯示 This list is empty。</li>
<li>任何繪製邏輯的更改。</li>
</ul>
<p>
下面來看一個實際場景,使用 Weather 元件讀取天氣預報資料,並進行天氣資訊展示,程式如下。
</p>
<pre><code class="language-js">
import axios from 'axios'
class Weather extends Component {
constructor (props){
super (props)
this.state = {
temperature: 'N/A',
windSpeed: 'N/A'
}
}
componentDidMount () {
axios.get('http://weather.com/api').then(response =>{
const { current } = response.data
this.setState ({
temperature: current.temperature,
windSpeed: current.windSpeed
})
})
}
render (){
const { temperature, windSpeed } = this.state
return (
<div className="weather">
<div>Temperature: {temperature} °C</div>
<div>Wind: {windSpeed} km/h</div>
</div>
)
}
}
</code></pre>
<p>
這個元件很容易了解,並且看上去沒什麼大問題,只是並不符合單一職責。 舉例來說,這個 Weather
元件將資料取得與繪製邏輯耦合在了一起,如果資料請求有變化,就需要在 componentDidMount
生命週期中進行改動;如果展示天氣的邏輯有變化,就需要對 render 方法進行更改。
</p>
<p>
如果我們將這個元件拆分成 WeatherFetch 和 WeatherInfo
兩個元件,這兩個元件各自只需要做一件事,保持單一職責,則重構後 程式如下。
</p>
<p></p>
<pre><code class="language-js">
import axios from 'axios'
import WeatherInfo from '/weatherInfo'
class WeatherFetch extends Component {
constructor (props) {
super(props)
this.state = { temperature: 'N/A', windSpeed: 'N/A' }
}
componentDidMount () {
axios.get('http://weather.com/api').then(response =>{
const { current } = response.data
this.setState({
temperature: current.temperature, windSpeed: current.windSpeed})
})
}
render (){
const {temperature, windSpeed} = this.state
return (
<WeatherInfo temperature={temperature} windSpeed={windSpeed} />
)
}
}
</code></pre>
<p>在另一個檔案中實現 WeatherInfo 元件的程式如下。</p>
<pre><code class="language-js">
const WeatherInfo = ({temperature, windSpeed })=>(
<div className="weather">
<div>Temperature: {temperature} °C</div>
<div>Wind: {windSpeed} km/h</div>
</div>
)
</code></pre>
<p>
如果我們想對程式進行重構,使用 async/await 代替 Promise,那麼只需要直接更改 WeatherFetch
元件即可,不會對 WeatherInfo 元件有任何影響,程式如下。
</p>
<p></p>
<pre><code class="language-js">
class WeatherFetch extends Component{
async componentDidMount () {
const response = await axios.get('http://weather.com/api')
const { current } = response.data
this.setState({
temperature: current.temperature,
windSpeed: current.windSpeed
})
}
// ...
}
</code></pre>
<p>
相對地,如果將顯示風速的邏輯從 "Wind: 0 km/h" 改為文字描述「Wind:風平浪靜」,也只需要改動
WeatherInfo 元件,而不會對 WeatherFetch 元件有任何影響,程式如下。
</p>
<p>
這只是一個簡單的實例,在真實的專案中,保持元件的單一職責非常重要,甚至可以使用 HOC
強制元件保持單一職責。
</p>
<pre><code class="language-js">
class PersistentForm extends Component{
constructor (props) {
super (props)
this.state = {
inputValue: localStorage.getItem('inputValue') }
this.handleChange = this.handleChange.bind(this)
this.handleClick = this.handleClick. bind(this)
}
handleChange(event) {
this.setState ({
inputValue: event.target.value
})
}
handleClick () {
localStorage.setItem('inputValue', this.state.inputValue)
}
render () {
const { inputValue } = this.state
return (
<div className="persistent-form">
<input type="text" value={inputValue} onChange={this.handleChange} />
<button onClick={this.handleClick}>
Save to storage </button>
</div>
)
}
}
</code></pre>
<p>
與初始實現相比,以上程式中的改動如下:初始 state 的值不再透過直接讀取 localStorage
提供,而是由 this.props.initialValue 提供;handleClick 對 this. props.saveValue
進行邏輯呼叫,而不再直接操作 localStorage。更進一步地,我們可以用 withPersistence 這個 HOC
來實現 this.props.saveValue 方法,程式如下。
</p>
<pre><code class="language-js">
function withPersistence (storageKey, storage) {
return function (WrappedComponent) {
return class PersistentComponent extends Component {
constructor (props) {
super(props)
this.state = initialValue: storage.getItem(storageKey)
}
render () {
return(
<WrappedComponent initialValue={this.state.initialValue} saveValue={this.saveValue}
{...this.props}
/>
);
}
saveValue(value) {
storage.setItem(storageKey, value)
}
}
}
}
</code></pre>
<p>withPersistence 元件的使用方式如下所示。</p>
<pre><code class="language-js">
const LocalStoragePersistentForm = withPersistence('key',localStorage)(PersistentForm)
</code></pre>
<p>
這種方式是元件單一職責和元件重複使用的結合的表現,其他元件當然也可以使用這個
HOC,程式如下。
</p>
<pre><code class="language-js">
const LocalStorageMyOtherForm = withPersistence('key', localStorage)(MyOtherForm)
</code></pre>
<p>
上述關於 withPersistence
元件的設計和使用方法對儲存和繪製職責進行了解耦,這樣便可以隨時切換儲存方式,舉例來說,將
localStorage 打換為 sessionStorage,程式如下。
</p>
<pre><code class="language-js">
const SessionStoragePersistentForm = withPersistence('key', sessionStorage)(PersistentForm)
</code></pre>
<h3>元件通訊和封裝</h3>
<p>
元件連結有緊耦合和鬆散耦合之分,緊耦合是指兩個或多個元件之間需要了解彼此的元件內設計,這樣的情況是我們不想看到的,因為它破壞了元件的獨立性,「牽一髮而動全身」。這麼看來,鬆散耦合帶來的好處是很直接的。
</p>
<p>直接來看一個簡單的計數器元件的實現。</p>
<pre><code class="language-js">
class App extends Component {
constructor(props){
super (props)
this.state = { number: 0 }
}
render (){
return(
<div className="app">
<span className="number"> {this.state.number}</span>
<Controls parent={this} />
</div>
)
}
}
class Controls extends Component {
updateNumber(toAdd){
this.props.parent.setState(prevState =>({
number: prevState.number + toAdd
}))
}
render(){
return (
<div className="controls" >
<button onClick={ ()=> this.updateNumber(+1) }>
Increase </button>
<button onClick={ ()=> this.updateNumber(-1) }>
Decrease </button>
</div>
)
}
}
</code></pre>
<p>
這樣的元件實現的問題很明顯:App元件不具有封裝性,它將實例傳給 Controls 元件,Controls
元件可以直接更改App state 中的內容。事實上,我們並不是不允許Controls
元件修改App元件中的內容,只是不建議使用 Controls 元件直接呼叫 App 元件的 setState
方法,因為 Controls 元件如果要呼叫 App 的 setState,就需要知道 App 元件 state
的結構,需要了解 this.props.parent.state.number 等詳情。
</p>
<p>
同時,上述程式也不利於測試,這一點將在後面說明。那麼應該如何重構呢?答案就是秉承封裝性,使元件的
state 結構只有自己知道,將 updateNumber 遷移至 App 元件內,程式如下。
</p>
<pre><code class="language-js">
class App extends Component {
constructor (props){
super(props)
this.state = { number: 0}
}
updateNumber (toAdd) {
this.setState(prevState => ({
number: prevState.number + toAdd
}))
}
render () {
return (
<div className="app" >
<span className="number"> {this.state.number}</span>
<Controls
onIncrease={ ()=> this.updateNumber(+1)}
onDecrease={ ()=> this.updateNumber(-1)}
/>
</div>
)
}
}
const Controls = ({ onIncrease, onDecrease }) => {
return (
<div className="controls">
<button onClick={onIncrease}>Increase</button>
<button onClick={onDecrease}>Decrease</button>
</div>
)
}
</code></pre>
<p>
這樣一來,Controls 元件就不需要知道 App
元件的內部情況了,實現了更好的重複使用性和可測試性,App 元件因此也具有了更好的封裝性。
</p>
<h3>元件組裝性設計</h3>
<p>React 具有天生的組合基因,可以將一個頁面拆分成許多個元件,如圖:</p>
<img src="./assets/image/1-6-2/4.jpeg" alt="" />
<p>上圖整個頁面對應的宣告式程式如下。</p>
<pre><code class="language-js">
const app = (
<Application>
<Header />
<Sidebar>
<Menu />
</Sidebar>
<Content>
<Article />
</Content>
<Footer />
</Application>
)
</code></pre>
<p>
如果兩個元件 Composed1 和 Composed2 具有相同的邏輯,則可以使用組合性進行拆分重組,如下
</p>
<pre><code class="language-js">
const instance1 = (
<Composed1>
// Composed1邏輯
// 重複邏輯
</Composed1>
)
const instance2 = (
<Composed2>
//重複邏輯
// Composed2邏輯
</Composed2>
)
</code></pre>
<p>將重複邏輯分析為 Common元件,程式如下。</p>
<pre><code class="language-js">
const instance1 = (
<Composed1>
<Logic1 />
<Common />
</Composed1>
)
const instance2 = (
<Composed2>
<Common />
<Logic2 />
</Composed2>
)
</code></pre>
<p>另外一個組合性的典型應用就是 render prop 模式</p>
<pre><code class="language-js">
const ByDevice = ({children :{mobile, other }})=>{
return Utils.isMobile() ? mobile : other
}
<ByDevice>
{{
mobile: <div>Mobile detected!</div>,
other: <div>Not a mobile device</div>
}}
</ByDevice>
</code></pre>
<h3>副作用和(準)純元件</h3>
<p>
簡單來說,透過函數參數能夠唯一確定函數傳回值的函數,我們稱之為純函數,反之就是有副作用的非純函數。將純/非純函數延伸到元件中,就是純/非純元件。
</p>
<p>
最好的情況是應用全部由純元件組成,這對元件的偵錯和穩固性非常重要。但這只是理想情況,在真實環境中,我們需要發送網路
</p>
<p>
請求以取得資料(因為資料不固定,需要從網路取得,這是一種副作用),定進行條件繪製等操作,如何大幅地位址應用由純元件或準純元件組成呢?先來下一個定義:
</p>
<p>準純元件是繪製資料全部來自 props,但是會產生副作用的元件。</p>
<p>從非純元件中分析純元件部分,是一個很常見且很有效的做法。下面來看一段非純元件的程式。</p>
<pre><code class="language-js">
const globalConfig = {
siteName: 'Animals in Zoo'
}
const Header = ({ children }) => {
const heading = globalConfig.siteName ?<h1>{globalConfig.siteName}</h1>:null
return (
<div>
{heading}
{children}
</div>
);
}
</code></pre>
<p>這個元件是典型的非純元件,因為它依賴全域變數 siteName。其可能繪製出以下兩種結果:</p>
<pre><code class="language-js">
// globalConfig.siteName存在時
<div>
<h1>Animals in 200</h1>
Some content
</div>
// globalConfig.siteName 不存在
<div>
Some content
</div>
</code></pre>
<p>在撰寫測試使用案例時,還需要考慮 globalConfig.siteName 的值,這會使邏輯更加複雜。</p>
<pre><code class="language-js">
import assert from 'assert'
import { shallow } from 'enzyme'
import globalConfig from './config'
import Header from './Header'
describe('<Header />',function(){
it('should render the heading',function(){
const wrapper = shallow(
<Header>Some content</Header>
)
assert(wrapper.contains(<h1>Animals in zoo</h1>))
})
it('should not render the heading',function(){
// 改動全域變數
globalConfig.siteName = null
const wrapper = shallow(
<Header>Some content</Header>
)
assert(appWithHeading.find('h1').length === 0)
})
})
</code></pre>
<p>在測試 Header 元件時,不僅需要多測試一種案例,還需要我們手動改寫全域變數的值。</p>
<p>
一個常用的最佳化方式是使全域變數作為 Header 的 props
出現,而不再是一個外部的全域變數,這樣一來,函數式元件 Header
就完全依賴其參數來決定繪製內容了,程式如下所示。
</p>
<pre><code class="language-js">
const Header = ({children, siteName })=>{
const heading = siteName ? <h1>{siteName}</h1> : null;
return (
<div className="header">
{heading} {children}
</div>
)
}
Header.defaultProps = {
siteName: globalConfig.siteName
}
</code></pre>
<p>測試使用案例便可簡化如下所示:</p>
<pre><code class="language-js">
import assert from 'assert'
import { shallow } from 'enzyme'
import { Header } from './Header';
describe('<Header />', function () {
it ('should render the heading', function () {
const wrapper = shallow(
<Header siteName="Animals in Zoo">Some content</Header>
)
assert(wrapper.contains(<h1>Animals in Zoo</h1>))
})
it('should not render the heading',function(){
const wrapper = shallow(
<Header siteName={null}>Some content</Header>
)
assert(appWithHeading.find('h1').length === 0)
})
})
</code></pre>
<p>這時,我們不用手動改變變數的值,就可以完成測試邏輯了。</p>
<p>
另一個重構非純元件的典型案例是針對有網路請求的副作用情況的,下面重新放一下在元件單一職責中的程式。
</p>
<pre><code class="language-js">
import axios from 'axios'
import WeatherInfo from './weatherInfo'
class WeatherFetch extends Component {
constructor (props) {
super(props)
this.state = { temperature: 'N/A', windSpeed: 'N/A' }
}
componentDidMount (){
axios.get('http://weather.com/api').then(response=>{
const { current } = response.data
this.setState {
temperature: current.temperature,
windSpeed: current.windSpeed
}
})
}
render () {
const { temperature, windSpeed } = this.state
return (
<WeatherInfo temperature={temperature} windSpeed={windSpeed} />
)
}
}
</code></pre>
<p>
表面上看,WeatherFetch
元件不得不作為非純元件,因為網路請求不可避免,但是我們可以將請求的主體邏輯分離出元件,使元件只負責呼叫請求,我將這樣的元件稱為準純元件,這種拆分非純元件的程式實現如下。
</p>
<pre><code class="language-js">
import { connect } from 'react-redux'
import { fetch } from './action'
export class WeatherFetch extends Component {
render () {
const { temperature, windSpeed } = this.props
return(<WeatherInfo temperature={temperature} windSpeed={windSpeed} />)
}
componentDidMount () {
this.props.fetch()
}
}
function mapStateToProps(state) {
return{
temperature: state.temperate,
windSpeed: state.windSpeed
}
}
export default connect(mapStateToProps, { fetch })
</code></pre>
<p>
我們使用 Redux 來完成整體頁面邏輯,這樣一來 WeatherFetch 元件至少可以確保相同的 props
會繪製出相同的結果,那麼對 WeatherFetch 元件的測試也就變得更加簡單清晰。測試程式如下。
</p>
<pre><code class="language-js">
import assert from 'assert'
import { shallow, mount } from 'enzyme'
import { spy } from 'sinon'
import { WeatherFetch } from './WeatherFetch';
import WeatherInfo from './WeatherInfo'
describe('<WeatherFetch />', function () {
it('should render the weather info', function () {
function noop () {}
const wrapper = shallow(
<WeatherFetch temperature="30" windSpeed="10" fetch={noop} />
)
assert(wrapper.contains(
<WeatherInfo temperature="30" windSpeed="10" />
))
})
it('should fetch weather when mounted',function(){
const fetchSpy = spy()
const wrapper = mount(
<WeatherFetch temperature="30" windSpeed="10" fetch={fetchSpy} />
)
assert(fetchSpy.calledOnce)
})
})
</code></pre>
<h3>元件可測試性</h3>
<p>
我們一直在提「可測試性」,上面也列出了測試使用案例程式,我認為是否具有測試意識,是區別進階程式設計師和一般程式設計師的標準之一。
</p>
<p>還記得上面提到的 Controls 元件嗎?它的最初實現程式如下。</p>
<pre><code class="language-js">
class App extends Component {
constructor (props) {
super(props)
this.state = { number: 0 }
}
render () {
return (
<div className="app">
<span className="number">{this.state.number} </span>
<Controls parent={this} />
</div>
)
}
}
class Controls extends Component {
updateNumber(toAdd) {
this.props.parent.setState(prevState => ({
number: prevState.number + toAdd
}))
}
render(){
return (
<div className="controls">
<button onClick={()=>this.updateNumber(+1)}>
Increase
</button>
<button onClick={()=>this.updateNumber(-1)}>
Decrease
</button>
</div>
)
}
}