全局狀態管理:如何再函數組件中使用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 仍然是一項必須掌握的既能。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM