-
Notifications
You must be signed in to change notification settings - Fork 0
/
1-6-3.html
312 lines (308 loc) · 19.9 KB
/
1-6-3.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
<!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-3">1-6-3 React: 資料狀態管理<h2>
<p>為什麼需要資料狀態管理,資料狀態管理到底在解決什麼樣的問題?這其實是架構、元件化帶來的概念。還是舉先前案例:在一個頁面上,點擊其中一處「訂閱」按鈕後,真面中的其他「訂閱」按鈕也會切換為「已訂閱」狀態。如果沒有資料狀態,也許就需要用以下方式來實現訂閱需求。</p>
<h3 ></h3>
<pre><code class="language-js">
const btnEle1 = $('#btn1')
const btnEle2 = $('#btn2')
btnEle1.on('click', () => {
if(btnEle1.textContent ==='已訂閱'){
return
}
btnEle1.textContent ='已訂閱'
btnEle2.textContent ='已訂閱'
})
btnEle2.on('click', () => {
if(btnEle2.textContent ==='已訂閱'){
return
}
btnEle1.textContent ='已訂閱'
btnEle2.textContent ='已訂閱'
})
</code></pre>
<p>這只是兩個按鈕的情況,處理起來就非常混亂了,而且難以維護,在這種情況下非常容易滋生 bug。</p>
<p>現代化的架構解決這個問題的想法是元件化,元件依賴資料,對應這個場景的資料狀態的程式如下。</p>
<pre><code class="language-js">
hasMarked: false / true
</code></pre>
<p>根據 hasMarked 這個狀態值,所有的訂閱元件都可以回應正確的視圖操作。把麵條式的程式轉換成可維護的程式後,對資料的管理就成了重中之重,這就是資料狀態的雛形。但是,一旦資料量越來越大,如何與元件形成良好的互動就是一種學問了。舉例來說,要考慮以下情況。</p>
<ul>
<li>一個元件需要和另一個元件共用狀態。</li>
<li>一個元件需要改變另一個元件的狀態。</li>
</ul>
<p>以 React 為例,其他架構類似,如果 React 自己來維護這些資料,則資料狀態就是一個物件,並且這個物件在元件之間要互相修改,極其混亂。</p>
<p>要考慮一個問題:hasMarked 這種資料到底是應該放在 state中維護,還是應該借助資料狀態管理類別庫,如 Redux,來維護呢?至少這樣一來,資料來源是單一的,資料狀態和元件是解耦的,也更加方便開發者進行偵錯和擴充資料。</p>
<p>以 React 的state 和Redux為例,來分析一下「資料由誰來維護」的問題。</p>
<p>React 的 state 是在元件內部維護的資料,當某項 state 需要與其他元件共用時,我們就可以透過 props 來完成元件間的通訊。從實作上來看,這就需要相對頂層的元件維護共用的 state 並提供修改此項 state 的方法。state 本身和修改方法都需要透過 props 傳遞給子孫元件。
</p>
<p>使用 Redux 的時候,在 Redux store 中維護資料。任何需要存取並更新資料的元件都需要連線 Redux,完成對 Redux store 的訂閱,這通常借助容器元件來完成。Redux 對資料採用集中管理的方式。</p>
<p>從資料持久度、資料消費範圍的角度來回答這個問題。首先,在資料持久度上,不同狀態的資料大致可以分為以下3大類。</p>
<ul>
<li>快速變更型:這種資料在應用中代表了某些原子等級的資訊,且顯著特點是變更頻率最快。舉例來說,一個文字輸入框中的資料值可能會隨著使用者輸入在短時間內持續發生變化。這種資料顯然更適合在 React 元件內維護。</li>
<li>中等持續型:在使用者瀏覽或使用應用時,這種資料常常會在頁面更新前保持穩定。舉例來說,從非同步請求介面透過 AJAX方式得來的資料,或使用者在個人中心頁編輯提交的資料。這種資料較為通用,也許會被不同元件所使用。資料在 Redux store 中維護,並透過 connect 方法和元件進行連接,是一種不錯的選擇。</li>
<li>長遠穩定型:指在頁面多次更新或多次存取期間都保持不變的資料。因為
Redux store 會在每次真面掛載後都重新產生一份新的資料,因此這種類型的資料顯然應該儲存在Redux以外的地方,如伺服器端資料庫或瀏覽器的本機存放區。</li>
</ul>
<p>即資料消費範圍上來分析。資料特性表現在消費層面,即有多少元件需要使用。我們以此來區分 Rcact 和 Redux 的不同分工。
廣義上,需要消費同一種資料的元件越多,在 Redux store 中維護這種資料就越合理;反之,如果某種資料與其他資料隔離,只服務於應用中某單一部分,那麼由 React 維護更加合理。</p>
<p>實際來看,共用的資料應該存在於 React 的高層元件中,由此在低層元件一層層傳遞。如果在 props 傳遞深度上只需要一兩個層級就能滿足消費資料的元件需求,那麼這樣的跨度是可以接受的;反之,如果跨越層級很多,那麼連結到的所有中間層級的元件就都需要進行接力賽式的傳遞,這樣顯然會增加很
多乏味的傳遞程式,也破壞了中間元件的重複使用性。這時,使用Redlx維護共用狀態,合理設定容器元件,透過 comeet來打通資料,就是一種更好的方式。</p>
<p>如果一些完全不存在父子關係的元件需要共用資料,舉例來說,前面提到過的真面需要在多處展示使用者圖示,那麼常常會造成資料輻射分散的問題,對於React 模式的狀態管理十分不利。在這種場景下,使用 Redux 同樣是更好的選擇。</p>
<p>如果應用有追蹤狀態的功能,舉例來說,需要完成「重放」「返回」或 "Redo/Undo" 等需求,那麽使用 Redux 無疑是最佳選擇,因為 Redux 天生擅長於此:每一個 action 都描述了資料狀態的改變和更新,非常便於進行資料的集中管理。</p>
<p>Redux 可以被任何一個資料管理類別庫所取代。</p>
<h3>Redux使用方式</h3>
<p>先上模板</p>
<pre><code class="language-html">
<ShoppingApp>
<Header />
<SideMenu />
<ProductsContainer>
</ProductsContainer>
</ShoppingApp>
</code></pre>
<img src="./assets/image/1-6-3/1.jpeg" alt="" />
<p>ProductsContainer 元件負責繪製每一個商品項目。</p>
<pre><code class="language-js">
import Product from './Product'
export default class ProductsContainer extends Component {
constructor (props) {
super(props);
this.state = {
products: [
'商品1',
'商品2',
'商品3'
]
}
}
renderProducts () {
return this.state.products.map((product) => {
return <Product name={product} />
})
}
render () {
return (
<div className='products-container'>
{this.renderProducts()}
</div>
)
}
}
</code></pre>
<p>Product 元件作為 UI 元件/展示元件,負責接收資料、展示資料。這樣一來,Product 元件即可用函數式/無狀態元件完成撰寫。</p>
<pre><code class="language-js">
import React, { Component } from 'react'
export default class Product extends Component {
render(){
return (
<div className='product'>
{this.props.name}
</div>
)
}
}
</code></pre>
<p>使用 React state 就可以實現上面的設計需求,且合理高效。</p>
<p>但是,如果商品有「立即購買」按鈕,點擊「立即購買」後會將商品加入購物車(對應上面 Cart Info 部分),這時需要注意,購物車中的商品資訊會在多個頁面(如下所示)被消費。</p>
<ul>
<li>
右上角需要展示購物車中商品數目的目前頁面
</li>
<li>
購物車頁面本身
</li>
<li>
支付前的確認頁面
</li>
<li>
支付頁面
</li>
</ul>
<p>這就是單頁面應用需要對資料狀態進行管理的訊號。我們可以維護一個cartList陣列以供應用消費,這個陣列放在 Redux 或 Vuex 中都是可行的。</p>
<h4>合理的 connect 場景</h4>
<p>在使用 Redux時,我們可以搭配 React-Redux 這個類別庫來聯通(connect)元件和資料,但是容易陷入的常見錯誤就是濫用 connect ,而沒有進行更合理的設計分析,或只在頂層進行 connect 設計,然後需要一層一層地進行資料傳遞。</p>
<p>舉例來說,在一個頁面中存在 Profile、Feeds(資訊流)、Images(圖片)區域,如下圖</p>
<img src="./assets/image/1-6-3/2.jpeg" alt="" />
<p>這些區域組成了頁面的主體,它們分別對應於 Profile、Feeds、Images 元件,共同作為 Page 元件的子元件而存在,對應的程式如下。</p>
<pre><code class="language-js">
<Page>
<Profile />
<Feeds />
<Images />
</Page>
</code></pre>
<p>如果只對 Page 這個頂層元件進行 connect 設計,其他元件的資料依靠 Page 元件進行分發,則設計如下圖所示</p>
<img src="./assets/image/1-6-3/3.jpeg" alt="" />
<p>這樣做存在的問題如下</p>
<ul>
<li>當改動 Profile 元件中的使用者圖示時,由於資料變動,整個Page 元件都會被重新繪製。</li>
<li>當刪除 Feeds 元件中的一筆資訊時,整個 Page 元件也都會被重新繪製。</li>
<li>當在 Images 元件中增加一張圖片時,整個 Page元件同樣都會被重新繪製。</li>
</ul>
<p>因此,更好的做法是對 Profile、Feeds、Images 這3個元件分別進行 connect
設計,在 connect 方法中使用 mapState ToProps 篩選出不同元件關心的 state 部分,如圖</p>
<img src="./assets/image/1-6-3/4.jpeg" alt="" />
<p>這樣做的好處很明顯,實際如下。</p>
<ul>
<li>當改動 Profile 元件中的使用者圖示時,只有 Profile 元件會被重新繪製。</li>
<li>當刪除 Feeds 元件中的一筆資訊時,只有 Feeds元件會被重新繪製。</li>
<li>當在 Images 元件中增加一張圖片時,只有 Images 元件會被重新繪製。</li>
</ul>
<h4>扁平化資料狀態</h4>
<p>扁平化的資料結構是一個很有意義的概念,它不僅能夠合理啟動開發邏輯,同時也是效能最佳化的一種表現。請看如下所示的資料結構。</p>
<pre><code class="language-js">
{
articles:[{
comments: [{
authors: [{
}]
}]
}],
// ...
}
</code></pre>
<p>這是一個文章列表加文章評論互動的場景,其對應於3個元件:Article、Comment 和Author。這樣的真面設計比比皆是,如下圖</p>
<img src="./assets/image/1-6-3/5.jpeg" alt="" />
<p>按照上述資料結構,可以想像,reducer 函數的相關資料處理就很棘手了。如果 articles[2].comments[4].authors1 發生了變化,那麼想要傳回更新後的狀態,並確保不可變性,操作起來就沒有那麼簡單了,我們需要對深層物件結構進行拷貝或遞迴。</p>
<p>因此,更好的資料結構設計一定是扁平化的,我們可以對 articles、comments、authors
進行扁平化處理,舉例來說,使 comments 陣列不再儲存 authors 資料,而是記錄 userId 評論內容中需要使用者資料時,只需在 users 陣列中按照 userId 進行分析即可。</p>
<pre><code class="language-js">
{
articles: [{
// ...
]},
comments: [{
articleId:''
userId: '',
}],
users: [{
// ...
}]
}
</code></pre>
<p>不同元件只需要關心不同的資料片段,舉例來說 Comment 元件只關心 comments 陳列 Author 元件只關心 users 陣列。這樣不僅操作更合理,而目能有效減少繪製壓力。</p>
<h3>Redux 優缺點</h3>
<p>歸納一下,Redux的限制主要表現在以下5個方面:</p>
<ul>
<li>Redux帶來了函數式程式設計、不可變性思懇等,為了配合這些理念,開發者必須要寫很多「模式程式」(boilerplate),繁瑣及重複是開發者不願意容忍的。當然,也有很多巧妙的方法旨在減少 boilerplate,但目前來看,可以說Redux 天生就帶有繁瑣的基因。</li>
<li>使用 Redux時,應用需要使用 objects或 arrays 描述狀態。</li>
<li>使用 Redux時,應用需要使用 plain objects 及 actions 來描述變化。</li>
<li>使用 Redux時,應用需要使用純函數去處理變化。</li>
<li>要將應用中的很多狀態抽象到 store,不能痛痛快快地寫業務程式,出現一個變化就要撰寫對應的 action(action creator)、reducer 等。</li>
</ul>
<p>和響應式結合函數式的 Mobx相比,這些缺點使 Redux 的程式設計體驗大打折扣。</p>
<p>為了彌補這些缺點,社區開啟了一輪又一輪的嘗試,其中一個努力方向是基於 Redux封裝一整套上層解決方案,這個方向上的成果以Redux-sage、Dva、Rematch 類別庫或架構為主。</p>
<p>下面歸納一下這些解決方案的特點和想法。</p>
<h4>1. 簡化初始化過程</h4>
<p>傳統的 Redux 初始化充滿了一些巧妙的方法,過於函數式,且較為繁瑣,程式如下</p>
<pre><code class="language-js">
import {createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import rootReducer from 'reducers'
const initialState = {
// ...
}
const store = initialState => createStore (
rootReducer, initialState, compose(
applyMiddleware(thunk),
// ....
)
)
</code></pre>
<p>這裡只應用了一個中介軟體,還沒有涉及 devtool 的設定,它類似 Dva 的 Redux 代替品,採用物件導向式的初始化設定。</p>
<h4>2. 簡化 reducer</h4>
<p>傳統的 reducer 可能需要撰寫惱人的switch.case 或很多樣板程式,而以Redux 為基礎的上層解決方案對 reducer 邏輯進行了封裝,以便使用起來能夠更加簡潔高效。</p>
<pre><code class="language-js">
const reducer = {
ACTIONTYPE1:(state, action) => newState,
ACTIONTYPE2:(state, action) => newState,
}
</code></pre>
<h4>3. 帶請求的副作用</h4>
<p>Redux 一般需要使用 thunk 中介軟體來處理網路請求,它的原理是:首先傳遞一個action,但是這個 action 不是 plain object 類型,而是一個函數;thunk 中介軟體發現 action type 為函數類型時,會把 dispatch和 getState 等方法作為參數傳遞給函數進行副作用邏輯。</p>
<p>如果不是React、Redux 的開發者:如許很那看懂上面一段描述,這也是 Redux 處理含有副作用的邏輯較為複雜的表現,更上層的解決方案 Redux-saga 採用 Generator(產生)的思想或 async/await 處理副作用,無疑更加人性化合理。</p>
<p>為了更進一步地配合產生器方案,上層方案將 action 分為普通 action 和副作用 action,開發者使用起來也更加清晰。</p>
<h4>4. 將 reducer 和 action 合併</h4>
<p>為了進一步減少模式程式,一個通用的做法是在 Redux 之上,將 reducer 和 action 宣告合併,這樣可以使宣告一步合格,如下所示</p>
<pre><code class="language-js">
const store = {
state: {
count: 0,
state1: {}
},
reducers: {
action1:(state, action) => newState,
action2:(state, action) => newState
}
}
</code></pre>
<p>這裡定義了兩個 action:action1 和 action2。它們出自 store.reducers 的鍵名,而對應鍵值即 reducer 邏輯。</p>
<p>這些都是以 Redux 為基礎進行封裝的上層解決方案的基本思想,了解了這些,就不會再對 Dva、Redux-saga 的原理感到陌生了</p>
<p>理清了資料狀態管理的意義,簡化了資料管理的操作後,還要分析到底應該如何組織資料。</p>
<p>經過整理,基本已經確定開發者真正的核心訴求只是:方便地修改資料,方便地取得資料。僅此而已</p>
<h3>新的發展趨勢:Mobx</h3>
<p>對核心訴求申的修改資料來說,Redux 建議以函數式、不可獲性、資料扁平化的形式進行;而其中的取得資料說到底是依賴發佈/訂閱模式進行的。相對地,Mobx是物件導向和響應式的結合,它的資料來源是可變的,對資料的觀察是響應式的。下面來看一段用Mobx 處理資料的程式範例。</p>
<pre><code class="language-js">
const foo = observable ({
a:1,
b:2
})
autoRun(()=>{
console.log(foo.a)
})
foo.b= 3 // 沒有任何輸出
foo.a= 2 // 輸出:2
</code></pre>
<p>用 Mobx 處理資料原理是資料綁架/資料代理 。將上面的程式改為以下形式。</p>
<pre><code class="language-js">
const state = observable ({
state1: {}
})
autoRun(()=>{
return (<Component state1={state1} />)
})
state.state1 = {}
</code></pre>
<p>當修改state state1 時,將觸發 autoRun 的回呼,引起元件的重新繪數。不同於 Redux,這就是另一種流派 Mobx 的核心理念。</p>
<p>不管是 Redux 還是 Mobx,它們都做到了:元件可以讀取 state,修改state,且在有新 state 時更新。這裡的 state 都是單一資料來源,只不過修改 state 的方式不同。更進一步地說,Mobx 透過將物件和陣列包裝為可觀察物件,隱藏了大部分樣板程式,比 Redux 更加簡潔,更像是進行了雙向綁定。</p>
<p>在資料狀態不太複雜的情況下:使用 Mobx也許更加簡潔高效:但是如果資料狀態非常複雜,或你是函數式式設計的粉絲,則可以考慮使用 Redux ,當需要在 Redux 層面上進行封裝時,使用像 Dva 這樣的方案:是一個明智的選擇。</p>
<h4>如何做到脫離 Redux(context 和 hook)</h4>
<p>有兩種方法可以做到脱離 Redux:一是 Mobx 或 GraphQL,只是這樣做還是沒有脱離架構或類別庫;二是使用原生 React 方案,舉例來說,使用 context API,React 16.3 介紹了穩定版的 context 特性,它從某種程度上可以更方便地實現元件間通訊,尤其是對跨越多層父子元件的情況更加高效。Redux-React 就是以 context 實現為基礎的,那麼在一些簡單的情況下,完全可以使用穩定的 context,而拋棄 Redux。</p>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>