-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path1-6-4.html
340 lines (334 loc) · 18.2 KB
/
1-6-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
<!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-4">1-6-4 React: 新一代特性<h2>
<h3 >
React 現狀分析
</h3>
<p>歸納出3個特點。</p>
<ul>
<li>開發模式已經定型,有利於開發者持續學習。</li>
<li>仍然由強大的開發團隊進行維護,不斷帶來改變,這些改愛一方面使 React 更好,另一方面甚至推動了 JavaScript 語言的發展。</li>
<li>社區生態強大,有一系列解決方案,資料狀態管理、元件函數庫、伺服器端繪製生態群百花齊放。</li>
<ul>
<ul>
<li>概念越來越多。某種程度上,新舊概念並存,使學習曲線激增。</li>
<li>存在較多帶有 unsafe_ 標記的API ,使開發者始終擔憂相關 API 會有徹底放棄的那一天。</li>
<li>新特性的有點疑慮</li>
</ul>
<h3>從 React Component 看 React 發展史</h3>
<p>React Component 的發展主要經歷了以下3個階段。</p>
<ul>
<li>createClass 建立元件時期</li>
<li>ES class 宣告元件時期</li>
<li>無狀態(函數式)元件 + React hook 時期</li>
<ul>
<h4>createClass 建立元件時期</h4>
<p>createClass 是一個函數,接收參數並傳回元件實例。<p>
<pre><code class="language-js">
import React from 'react'
const component1 = React.createClass({
propTypes: {
foo: React.ProtoTypes.string
},
getDefaultProps () {
return {foo:'bar'}
},
getInitialState () {
return {
state1: 'lucas'
}
},
handleClick (){
},
render () {
return (
<p onclick={this.handleClick}></p>
)
}
})
</code></pre>
<p>以上程式看起來很好了解,但是撰寫還是有些違背直覺。從 React 15:5 版本開始,官方就不再開始推薦使用該函數,到了 React 16 版本,就將其徵底廢棄了。</p>
<h4>ES class 宣告元件時期</h4>
<p></p>當時 ES6 剛推出 class 特性就被 React 團隊所採用,取代了原本 createClass</p>
<pre><code class="language-js">
class Component1 extends React.Component{
handleClick = e => {
console.log(e)
state = {
name: 'Lucas'
}
this.setState ({
name:'Messi',
})
}
render () {
return (
<div onClick={this.handleClick}>
{{this.state.name}}
</div>
)
}
}
</code></pre>
<p>class 宣告方式和早期的 createClass 相比有非常重要的兩點差別,如下。<p>
<ul>
<li>React.createClass 支援在事件處理函數中自動綁定 this,而 class 宣告的元件需要開發者手動綁定。</li>
<li>React.Component 不能使用 React mixins 來實現重複使用。<li>
</ul>
<p>第一點差別決定了 React 放棄了多管閒事地綁定 this,雖然這個行為在很多人看來毫無必要,很多類別 React 架構都會幫助開發者對事件處理函數綁定 this ,Vue 也是如此。</p>
<p>綁定 this 的方案有很多種,上述程式採用了 ES Next 的屬性初始化方法,對 handleClick 進行了綁定。</p>
<p>第二點差別決定了 React 實現重複使用方式的發展方向。首先肯定的是,官方認為使用 mixins 是弊大於利的,所以已經做底放棄使用它。社區跟進的圍校使用方案主要有兩種,分別是使用高階元件和 render prop 模式。</p>
<p>使用 class 宣告元件並不是完美的。React 官方團隊認為,這種方式已背離了 React 的初衷。歸納了一下,class 宣告元件的問題有以下兩個。</p>
<ul>
<li>
帶來了針對生命週期式設計的因擾,隨著邏輯變複雜,元件的生命週期函數隨之變得很難維護和了解。我們想弄清楚 componentDidMount、
componentDidUpdate、componentWillUnmount 、componentWillReceiveProps
這些鉤子的邏輯並不困難。但是,這些生命週期函數中的程式和 render 中的 state 及 props 有什麼關係?這種問題將隨著應用越來越複雜被無限放大。</li>
<li>React 是函數式的,而 class 宣告元件這種物件導向的行為顯得不倫不類。</li>
</ul>
<p>基於這兩點,React 很快推出了函數式元件,(或無狀態元件,下面統稱為函數式元件,因為在 hook 特性下元件也會有狀態)。</p>
<h4>函數式元件</h4>
<p>函數式元件非常簡單,下面就用函數定義一個元件,該函數接收 props 作為參數,只負責繪製。</p>
<pre><code class="language-js">
const component = props => <div>{ props.name }</div>
</code></pre>
<p>然而它是完全無法取代 class 元件的,因為它不存在生命運期,完全的無狀態讓我們無法處理必要的邏輯。</p>
<h3>顛覆性的 React hook</h3>
<p>它可以使開發者按業務邏輯拆分程式,而非按生命週期。這樣如果根實現重複使用,直接在任何元件中引用相關 hook 即可。hook 把程式按照業務邏輯的相關性進行拆分,把同一業務的程式集中在一起,不同業務的程式獨立開來,維護起來就清楚很多。</p>
<h4>輕量級 useState</h4>
<p>事實上,setState API 並沒有什麼問題,也足夠輕量,真正笨重的是 class 元件與 setState 的結合。使用 useState hook 使得函數式元件也具備了操作 state 的能力,且不需要引用生命週期函數。</p>
<p>useState 是一個函數,其中的輸入參數是 initialState;它會傳回一個陣列,第一個值是 state,第二個值是改變 state 的函數。</p>
<p>這裡多說一個細節,為什麼 useState 會傳回一個陣列呢?如果傳回的是一個物件是否會更合適呢?這樣表意更加清晰且簡單,也支援我們自動設定別名。</p>
<pre><code class="language-js">
const React = (function () {
let stateValue
return Object.assign(React, {
useState (initialStateValue) {
stateValue = stateValue || initialStateValue
function setState (value){
stateValue = value
}
return [stateValue, setState]
}
})
})()
</code></pre>
<p>我們使用 stateValue 閉包變數儲存 state,並提供修改 state Value 的方法 setState,一併作為陣列傳回。</p>
<h4>useEffect 和生命週期那些事</h4>
<p>函數式元件透過
useState 具備了操控 state 的能力,修改 state 需要在適當的
場景下進行:class 宣告的元件在元件生命週期中進行 state 更迭,那麼在函數式元件中呢?我們需要用 useEffect 模擬生命週期。目前,useEffect 相當於綜合了 class Component 中的 componentDidMount、componentDidUpdate、component WillUnmount這3個生命週期。</p>
<p>也就是說,useEffect 宣告的回呼函數會在元件掛載、更新、移除的時候執行。為了避免每次繪製都執行所有的 useEffect 回呼,useEffect 提供了第二個參數,該參數是陣列類型。只有在繪製時陣列中的值發生了變化,才會執行該 useEffect 回呼。如果 useEffect 的第二個參數是個空陣列,也就是說目前資料狀態並不依賴任何其他資料,那麼 useEffect 就只會在元件第一次掛載後和
移除前被呼。</p>
<p>下面嘗試實現 useEffect,程式如下。</p>
<pre><code class="language-js">
const React = (function () {
let deps
return Object.assign(React, {
useEffect(callback, depsArray){
const shouldUpdate = !depsArray
const depsChange = deps ? !deps.every((depItem, index)=>depItem ===depsArray[index]) : true
if (shouldUpdate||depsChange){
callback()
deps = depsArray || []
}
}
})
})()
</code></pre>
<p>閉包變數 deps 會儲存前一刻 useEffect 的依賴陣列中的值。每次呼叫 useEffect
時,都會檢查 deps 陣列和目前 depsArray 傳列中的值,如果其中任何一項有變化,depsChange 的值都將為 true,進而執行 useEffect 的回呼。</p>
<p>那麼如何模擬生命週期 shouldComponentUpdate 呢?事實上:我們不需要用 useEffect 來實現 shouldComponentUpdate。React 新特性中專門提供了 React.memo來幫助開發者進行效能最佳化。另外,useEffect是無
法模擬 getSnapshotBeforeUpdate 和 componentDidCatch 兩個生命週期函數
的。</p>
<p>上述兩種實現都是簡易版的,旨在剖析這兩個 hook 的工作原理,很多細節都沒有實現。最重要的一點是,如果元件內多次呼叫 useState 或 useEffect,實現為了區分每次呼叫 useState 之前不同的 state 值及對應的 Setter 函數,就需要額外使用一個陣列來儲存每次呼叫的配對值,程式如下。</p>
<pre><code class="language-js">
const React =(function(){
let hooks =[]
let currentHook = 0
return Object.assign (React, {
useState(initialStateValue){
hooks[currentHook] = hooks[currentHook] || initialStateValue
function setState (value) {
hooks[currentHook] = value
}
return [hooks[currentHook++], setState]
},
useEffect (callback, depsArray) {
const shouldUpdate = depsArray
const depsChange = hooks[currentHook] ? !hooks[currentHook].every((depItem, index) => depItem === [depsArray[index]) : true
if (shouldUpdate || depsChange) {
callback ()
hooks[currentHook++] = depsArray ||[]
}
}
})
})()
</code></pre>
<p>這也是 hook 只可以在頂層使用,不能寫在循環本體、條件繪製,或巢狀結構 function 裡的原因。React 內部實現需要按呼叫順序來記錄每個 useState 的呼叫,以做區分。</p>
<h4>useReducer 和 Redux</h4>
<p>如果 State 的變化有比較複雜的狀態流轉,則可以使用新的hook:
useReducer 可以讓應用更加Redux化,使邏輯更加清晰。那麼首先思考一個問題:到底該用 useState 還是 useReducer 呢?為此,歸納如下。</p>
<p>使用 useState 的場景有以下幾種。</p>
<ul>
<li>state 為基本類型(也要看實際情況)。</li>
<li>state 轉換邏輯簡單的場景。</li>
<li>state 轉換只會在目前元件中出現,其他元件不需要感知這個 state。</li>
<li>多個 useState hook 之間的 state 並沒有連結關係。</li>
</ul>
<p>使用 useReducer 的場景有以下幾種。</p>
<ul>
<li>state 為參考類型(也要看情況)。</li>
<li>state 轉換邏輯比較複雜的場景。</li>
<li>不同 state 之間存在較強的連結關係,應該作為一個 object,用一個 state 來表示的場景。</li>
<li>需要更好的可維護性和可測試性的場景。</li>
</ul>
<p>翻看 React 原始程式中的 useState 實現就會發現,useState 本質上是 useReducer 的語法糖。</p>
<p>再思考第二個問題:useReducer 是否代表 React 內建了 Redux,以便我們可以脱離 Redux呢?事實上,確實可以用簡單的 React 程式,借助 context API 實現一個 React 附帶的全域 Redux 或局部 Redux。</p>
<p>在 store.js 檔案中設定 reducer 和資料初始狀態,程式如下所示。</p>
<pre><code class="language-js">
import React from 'react'
const store = React.createContext(null)
export const initialState = {
// ...
}
export const reducer = (state, action)=>{
switch (action.type) {
// ...
}
}
export default store
</code></pre>
<p>接著使用 Provider 來進行根元件掛載,程式如下。</p>
<pre><code class="language-js">
import React, { useReducer } from 'react'
import store, { reducer, initialState } from './store'
function App () {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<store.Provider value={{state, dispatch}} > <div/>
</store>
)
}
</code></pre>
<p>有了 store,業務元件就可以按照下面提式所示的方式來實現狀態管理了。</p>
<pre><code class="language-js">
import React, { useContext } from 'react'
import store from './store'
const Child = props =>{
const {state, dispatch } = useContext(store)
// ...
}
</code></pre>
<p>但是這樣的行為仍然不足以完全取代 Redux</p>
<h4>React hook 之 hook 之所以被設計為 hook 的原因</h4>
<p>總結以上內容</p>
<ul>
<li>useState:讓函數式元件能夠使用 state。</li>
<li>useEffect 讓函數式元件可以模擬生命週期方法,並進行副作用操作。</li>
<li>useReducer 讓我們能夠更清晰地處理狀態資料。</li>
<li>useContext 可以取得 context 值。</li>
</ul>
<p>React.memo 並沒有成為一個hook 原因其實是 React 認為能夠成為 hook 有兩個特定條件。</p>
<ul>
<li> 可組合:這個新特性需要具有組合能力,也就是說需要有重複使用價值,因為 hook的一大目標就是完成元件的重複使用。因此,開發者可以自訂hook,而不必使用官方指定的 hook。</li>
<li>可偵錯:hook 的一大特性就是能夠偵錯,如果應用出現差錯,要能夠從錯誤的 props 和 state 中找到錯誤的元件或邏輯,具有這樣偵錯功能的特性才有資格成為一個 hook。</li>
</ul>
<p>詳細可以看 Dan Abramoy 專門寫的文章 Why Isn' tX a HOok?</p>
<h3>令人期待的新特性</h3>
<p>React.Suspense 和 React.lazy。React.Suspense 給了 React 元任非同步(中斷)繪製的能力,打破了React 元件之前同步繪製整個元件的格局。而 React.lazy帶來了延遲載入的能力,可以極佳地取代社區上的一些輪子實現。</p>
<p>看一個場景,結合使用 React.Suspense 和 React.lazy 實現程式分割和隨選載入。
目前,隨選載入一般都採用 react-loadable,這個函數庫穩定優雅且支援伺服器端繪製,程式如下。</p>
<pre><code class="language-js">
const Loading = ({ delay }) => {
if(delay){
return <Spinner >
}
return null
}
export const AsyncComponent = Loadable({
loader:() => import(/* webpackChunkName:"Component1"*/'./component1'),
loading: Loading,
delay: 500
})
</code></pre>
<p>這段程式定義了一個 Loading 元件,在請求傳回之前進行伺服器端繪製; delay 參數表示時間超過 500ms 才顯示 Loading,防止閃爍 Loading的出現。</p>
<p>如果換成結合使用 React.Suspense 和 React.lazy 的方式・則程式如下。</p>
<pre><code class="language-js">
const Component = React.Lazy(() => import(/* webpackChunkName: "Component1"*/ './component1/'))
export const AsyncComponent = props =>(
<React.Suspense fallback={<Loading />}>
<Component {...props} />
</React.Suspense>
)
</code></pre>
<p>React.lazy 封裝動態 import 的 React 元件 要求 import 必須傳回一個 Promise
物件,並且這個 Promise 物件會被決議為一個ES 模組,模組的 export default 必須是一個合法的 React 元件。</p>
<p>React.Suspense 元件中設定了 fallback prop ,當發現 Component 是一個 Promise
類型,且這個 Promise 沒有被決議時,就啟用 fallback prop 所提供的元件,以便在等待網路傳回結果時進行伺服器端繪製。</p>
<p>我們可以結合 Error Boundary 特性,在出現網路錯誤或其他錯誤時進行錯誤處理。應用 Error Boundary 特性處理可能出現的錯誤的程式如下。</p>
<pre><code class="language-js">
<MyCustomErrorBoundary>
<AsyncComponent />
</MyCustomErrorBoundary>
</code></pre>
<p>這樣就實現了簡單的 react-loadable 函數庫。當然,React.Suspense 在最新的 React 18 已經正式推出。但透過自己手動實現一個 React.Suspense 元件可以更加了解其中背後設計的思路,下面的程式提供了一個簡單的版本,但未考慮邊界情況。
</p>
<pre><code class="language-js">
export class Suspense extends React.Component {
state = {
isLoading: false
}
componentDidCatch (error) {
if(typeof error.then === 'function') {
this.setState({ isLoading: true })
error.then(() => {
this.setState({ isLoading: false })
})
}
}
render () {
const { children, fallback } = this.props
const { isLoading } = this.state
return isLoading ? fallback: children
}
}
</code></pre>
<p>這段程式的核心想法就是,在第一次繪製 Promise出錯時使用 componentDid Catch 進行捕捉,然後透過狀態切換繪製 fallback 元件;在 Promise 決議之後,透過狀態切換繪製目標群元件。</p>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>