一、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 仍然是一項必須掌握的既能。