全域性狀態管理:如何再函式元件中使用Redux?
一、redux
Redux 作為一款狀態管理框架啊,是公認的 React 開發中最大的一個門檻,但同時呢,它也是 React 開發人員必須掌握的一項技能。因為只有熟練應用 Redux,你才能更加靈活地使用 React,來從容應對大型專案的開發難題。這裡我要說句題外話。Redux 誕生於 2015 年,也就是 React 出現之後一年多。雖然一開始是由第三方開發者開源,不是 Facebook 官方,但是也迅速成為了最為主流的 React 狀態管理庫。而且,之後 Redux 跟它的開發者 Dan Abbramov 和 Andrew Clark 一起,都被 Facebook 收編,成為 React 官方生態的一部分。側面可以看到 Redux 在 React 中的重要作用。需要說明的是,Redux 作為一套獨立的框架,雖然可以和任何 UI 框架結合起來使用。但是因為它基於不可變資料的機制,可以說,基本上就是為 React 量身定製的。不過你可能會說,Redux 上手比較難,該怎麼辦呢?的確是這樣,因 Redux 引入了一些新的程式設計思想,還有比較繁瑣的樣板程式碼,確實帶來了一定的上手難度。
但是你不要擔心,這篇文章,我會通過具體的例子帶你上手 Redux。而且我會講解 Redux 要解決什麼問題,引入了什麼樣的新概念,爭取能從本質上去理解 Redux 的理念和使用方法,提高你舉一反三的能力。
redux出現的背景
很多同學一開始可能不太明白狀態管理框架的作用。但是如果隨著對 React 使用的深入,你會發現元件級別的 state,和從上而下傳遞的 props 這兩個狀態機制,無法滿足複雜功能的需要。例如跨層級之間的元件的資料共享和傳遞。我們可以從下圖的對比去理解:
其中左圖是單個 React 元件,它的狀態可以用內部的 state 來維護,而且這個 state 在元件外部是無法訪問的。而右圖則是使用 Redux 的場景,用全域性唯一的 Store 維護了整個應用程式的狀態。可以說,對於頁面的多個元件,都是從這個 Store 來獲取狀態的,保證元件之間能夠共享狀態。所以從這張對比圖,我們可以看到 Redux Store 的兩個特點:
1、Redux Store是全域性唯一的。即整個應用程式一般只有一個store.
2、redux Store是樹結構,可以更天然的對映到元件樹的結構,雖然不是必須的。
我們通過把狀態放在元件之外,就可以讓 React 元件成為更加純粹的表現層,那麼很多對於業務資料和狀態資料的管理,就都可以在元件之外去完成(後面課程會介紹的 Reducer 和 Action)。同時這也天然提供了狀態共享的能力,有兩個場景可以典型地體現出這一點。
1.跨元件的狀態共享:當某個元件發起一個請求時,將某個loading的資料狀態設為true,另外一個全域性狀態元件則顯示Loading的狀態。
2.同組件多個例項的狀態共享:某個頁面元件初次載入時,會發送請求拿回了一個數據,切換到另一個頁面後又返回。這時資料已經存在,無需重新載入,設想如果時本地的元件state,那麼元件銷燬後重新建立,state也會被重置,就還需要重新獲取資料。
因此,學會Redux,才會真正用react去靈活解決問題,下面我們就來了解下redux中的一些基本概念。
理解redux的三個基本概念
redux引入的概念其實並不多,主要就是三個:state、Action和Reducer.
1.其中state即store,一般就是一個純js object.
2.Action也是一個Object,用於描述發生的動作。
3.而Reducer則是一個函式,接收Action和state並作為引數,通過計算得到新的Store.
他們三者之間的關係可以用下圖來表示:
在redux中,所有對於Store的修改都必須通過一個公式來完成,即通過Reducer完成,而不是直接該百年store,這樣的話某一方面可以保證資料的不可變性,同時也能帶來兩個非常大的好處
1.可預測性,即給定一個初始狀態和一系列的 Action,一定能得到一致的結果,同時這也讓程式碼更容易測試。
2.易與除錯:可以跟蹤 Store 中資料的變化,甚至暫停和回放。因為每次 Action 產生的變化都會產生新的物件,而我們可以快取這些物件用於除錯。Redux 的基於瀏覽器外掛的開發工具就是基於這個機制,非常有利於除錯。
這麼抽象的解釋,你可能不好理解,彆著急,我給你舉個例子,來幫助你理解這幾個概念。這個例子是開發一個計數器的邏輯。比如說要實現“加一”和“減一”這兩個功能,對於 Redux 來說,我們需要如下程式碼:
import { createStore } from 'redux' // 定義 Store 的初始值 const initialState = { value: 0 } // Reducer,處理 Action 返回新的 State function counterReducer(state = initialState, action) { switch (action.type) { case 'counter/incremented': return { value: state.value + 1 } case 'counter/decremented': return { value: state.value - 1 } default: return state } } // 利用 Redux API 建立一個 Store,引數就是 Reducer const store = createStore(counterReducer) // Store 提供了 subscribe 用於監聽資料變化 store.subscribe(() => console.log(store.getState())) // 計數器加 1,用 Store 的 dispatch 方法分發一個 Action,由 Reducer 處理 const incrementAction = { type: 'counter/incremented' }; store.dispatch(incrementAction); // 監聽函式輸出:{value: 1} // 計數器減 1 const decrementAction = { type: 'counter/decremented' }; store.dispatch(decrementAction) // 監聽函式輸出:{value: 0}
通過這個例子,我們看到了純 Redux 使用的場景,從而更加清楚地看到了 Store、Action 和 Reducer 這三個基本概念,也就能理解 State + Action => New State 這樣一個簡單卻核心的機制。
如何在React中Redux'
要知道,在實際場景中,Redux Store 中的狀態最終一定是會體現在 UI 上的,即通過 React 元件展示給使用者。那麼如何建立 Redux 和 React 的聯絡呢?
主要是兩點
1.React元件能夠在依賴的Store的資料發生變化時,重新Render
2.在React元件中,能夠在某些實際去dispatch一個action,從而觸發store的更新。
要實現這兩點,我們需要引入 Facebook 提供的 react-redux 這樣一個工具庫,工具庫的作用就是建立一個橋樑,讓 React 和 Redux 實現互通。在 react-redux 的實現中,為了確保需要繫結的元件能夠訪問到全域性唯一的 Redux Store,利用了 React 的 Context 機制去存放 Store 的資訊。通常我們會將這個 Context 作為整個 React 應用程式的根節點。因此,作為 Redux 的配置的一部分,我們通常需要如下的程式碼:
import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import store from './store' import App from './App' const rootElement = document.getElementById('root') ReactDOM.render( <Provider store={store}> <App /> </Provider>, rootElement )
這裡使用了 Provider 這樣一個元件來作為整個應用程式的根節點,並將 Store 作為屬性傳給了這個元件,這樣所有下層的元件就都能夠使用 Redux 了。完成了這樣的配置之後,在函式元件中使用 Redux 就非常簡單了:利用 react-redux 提供的 useSelector 和 useDispatch 這兩個 Hooks。在第二講我們已經提到,Hooks 的本質就是提供了讓 React 元件能夠繫結到某個可變的資料來源的能力。在這裡,當 Hooks 用到 Redux 時可變的物件就是 Store,而 useSelector 則讓一個元件能夠在 Store 的某些資料發生變化時重新 render。我在這裡仍然以官方給的計數器例子為例,來給你講解如何在 React 中使用 Redux:
import React from 'react' import { useSelector, useDispatch } from 'react-redux' export function Counter() { // 從 state 中獲取當前的計數值 const count = useSelector(state => state.value) // 獲得當前 store 的 dispatch 方法 const dispatch = useDispatch() // 在按鈕的 click 時間中去分發 action 來修改 store return ( <div> <button onClick={() => dispatch({ type: 'counter/incremented' })} >+</button> <span>{count}</span> <button onClick={() => dispatch({ type: 'counter/decremented' })} >-</button> </div> ) }
此外,通過計數器這個例子,我們還可以看到 React 和 Redux 共同使用時的單向資料流:
需要強調的是,在實際的使用中,我們無需關心 View 是如何繫結到 Store 的某一部分資料的,因為 React-Redux 幫我們做了這件事情。總結來說,通過這樣一種簡單的機制,Redux 統一了更新資料狀態的方式,讓整個應用程式更加容易開發、維護、除錯和測試。
使用Redux處理非同步邏輯
學完了如何在 React 中使用 Redux,接下來我們就進入到 Redux 的進階場景中。在 Redux 中,處理非同步邏輯也常常被稱為非同步 Action,它幾乎是 React 面試中必問的一道題,可以認為這是 Redux 使用的進階場景。雖然 Redux 的官方文件中已經將非同步邏輯的原理寫得很清楚,但是大部分同學仍然只能說個大概,或者蹦出 Thunk、Saga 之類的幾個單詞。造成這種現象的很大一部分原因可能在於,僅滿足於根據參考示例寫出可執行的程式碼,而沒有深究背後的原理。但是要明白一點,只有能夠解釋清楚非同步 Action,才算是真正理解了 Redux,才能在實際開發中靈活應用。
在 Redux 的 Store 中,我們不僅維護著業務資料,同時維護著應用程式的狀態。比如對於傳送請求獲取資料這樣一個非同步的場景,我們來看看涉及到 Store 資料會有哪些變化:
1.請求傳送出去時:設定 state.pending = true,用於 UI 顯示載入中的狀態;
2、請求傳送成功時:設定 state.pending = false, state.data = result。即取消 UI 的載入狀態,同時將獲取的資料放到 store 中用於 UI 的顯示。
3.請求傳送失敗時:設定 state.pending = false, state.error = error。即取消 UI 的載入狀態,同時設定錯誤的狀態,用於 UI 顯示錯誤的內容。
前面提到,任何對 Store 的修改都是由 action 完成的。那麼對於一個非同步請求,上面的三次資料修改顯然必須要三個 action 才能完成。那麼假設我們在 React 元件中去做這個發起請求的動作,程式碼邏輯應該類似如下:
function DataList() { const dispatch = useDispatch(); // 在元件初次載入時發起請求 useEffect(() => { // 請求傳送時 dispatch({ type: 'FETCH_DATA_BEGIN' }); fetch('/some-url').then(res => { // 請求成功時 dispatch({ type: 'FETCH_DATA_SUCCESS', data: res }); }).catch(err => { // 請求失敗時 dispatch({ type: 'FETCH_DATA_FAILURE', error: err }); }) }, []); // 繫結到 state 的變化 const data = useSelector(state => state.data); const pending = useSelector(state => state.pending); const error = useSelector(state => state.error); // 根據 state 顯示不同的狀態 if (error) return 'Error.'; if (pending) return 'Loading...'; return <Table data={data} />; }
從這段程式碼可以看到,我們使用了三個(同步)Action 完成了這個非同步請求的場景。這裡我們將 Store 完全作為一個存放資料的地方,至於資料哪裡來, Redux 並不關心。儘管這樣做是可行的。但是很顯然,傳送請求獲取資料並進行錯誤處理這個邏輯是不可重用的。假設我們希望在另外一個元件中也能傳送同樣的請求,就不得不將這段程式碼重新實現一遍。因此,Redux 中提供了 middleware 這樣一個機制,讓我們可以巧妙地實現所謂非同步 Action 的概念。簡單來說,middleware 可以讓你提供一個攔截器在 reducer 處理 action 之前被呼叫。在這個攔截器中,你可以自由處理獲得的 action。無論是把這個 action 直接傳遞到 reducer,或者構建新的 action 傳送到 reducer,都是可以的。
從下面這張圖可以看到,Middleware 正是在 Action 真正到達 Reducer 之前提供的一個額外處理 Action 的機會:
我們剛才也提到了,Redux 中的 Action 不僅僅可以是一個 Object,它可以是任何東西,也可以是一個函式。利用這個機制,Redux 提供了 redux-thunk 這樣一箇中間件,它如果發現接受到的 action 是一個函式,那麼就不會傳遞給 Reducer,而是執行這個函式,並把 dispatch 作為引數傳給這個函式,從而在這個函式中你可以自由決定何時,如何傳送 Action。
例如對於上面的場景,假設我們在建立 Redux Store 時指定了 redux-thunk 這個中介軟體:
import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import rootReducer from './reducer' const composedEnhancer = applyMiddleware(thunkMiddleware) const store = createStore(rootReducer, composedEnhancer)
那麼在我們 dispatch action 時就可以 dispatch 一個函式用於來發送請求,通常,我們會寫成如下的結構:
function fetchData() { return dispatch => { dispatch({ type: 'FETCH_DATA_BEGIN' }); fetch('/some-url').then(res => { dispatch({ type: 'FETCH_DATA_SUCCESS', data: res }); }).catch(err => { dispatch({ type: 'FETCH_DATA_FAILURE', error: err }); }) } }
那麼在我們 dispatch action 時就可以 dispatch 一個函式用於來發送請求,通常,我們會寫成如下的結構:
import fetchData from './fetchData'; function DataList() { const dispatch = useDispatch(); // dispatch 了一個函式由 redux-thunk 中介軟體去執行 dispatch(fetchData()); }
可以看到,通過這種方式,我們就實現了非同步請求邏輯的重用。那麼這一套結合 redux-thunk 中介軟體的機制,我們就稱之為非同步 Action。所以說非同步 Action 並不是一個具體的概念,而可以把它看作是 Redux 的一個使用模式。它通過組合使用同步 Action ,在沒有引入新概念的同時,用一致的方式提供了處理非同步邏輯的方案。
小結:
儘管 Redux 有令人詬病的地方,例如函式式的概念比較難以理解,樣板程式碼過多等問題。但其帶來的好處也是很明顯的,比如可以讓程式碼更容易理解,維護和測試。因此有超過 60% 的 React 應用都使用了 Redux。所以即使對於一些小型的應用,不一定需要使用 Redux。但是對於開發人員來說,學會和理解 Redux 仍然是一項必須掌握的既能。
勤學似春起之苗,不見其增,日有所長; 輟學如磨刀之石,不見其損,日所有虧!