對Redux一頭霧水?看完這篇就懂了

前些日子,我們翻譯了一篇 React 和 Vue 的對比文章:《我用 React 和 Vue 構建了同款應用,來看看哪裏不一樣》。最近,文章作者又撰寫了一篇與 Redux 對比的後續,我們也翻譯了這篇續文以饗讀者。

首先,學習 Redux 可能會很困難

當你終於學會了如何使用 React,也有了自己去構建一些應用的信心,那會是一種非常棒的感覺。你學會了管理狀態,一切看起來井井有條。但是,很有可能這就到了你該學習 Redux 的時候了。

這可能是因爲你正在開發的應用變得越來越大,你發現自己在到處傳遞狀態,還需要一種更好的方法來管理數據。或者也可能是,你發現一大堆招聘信息都寫着除了要會 React 以外,還得會 Redux。不管是哪種原因,瞭解如何使用 Redux 都是非常重要的知識,因此你應該努力去掌握它。

但是要搞懂 Redux 的原理就得研究一大堆新的代碼,實在很讓人頭痛。我個人還覺得常見的文檔(包括 Redux 的官方文檔)展示的 Redux 用法實在太多了,所以入門起來真的不容易。

從某種意義上說,這是一件好事,因爲它鼓勵你以自認爲合適的方式使用 Redux,不會有人跟你說“你應該用這種方法來做,否則你這開發者就太遜了”。但擁有這種美好的感覺的前提是,你得知道自己到底在用 Redux 做些什麼事情。

那麼我們該怎樣學習 Redux 呢?

在我之前對比 React 和 Vue 的文章中,使用了名爲 ToDo 的一款待辦事項列表應用做了演示。本文會繼續使用這種方法,只不過這次的主角換成了 Redux。

下面是 Redux 應用的文件夾結構,左邊是 React 版本的對比。

先來解釋一些 Redux 的基礎知識

Redux 基於三大原則來處理數據流:

1. 存儲

存儲(Store)也被稱爲單一可信源(single source of truth)。它在本質上只是你以某種狀態初始化的對象,然後每當我們要更新它時,我們都會用新版本覆蓋原有的存儲。總之,你可能已經在 React 應用中用到了這些理論,通常人們認爲最佳實踐是重新創建狀態而不是突變它。爲了進一步解釋這種區別我們舉個例子,如果我們有一個數組,並且想要將一個新項目推送進去,我們更新存儲時不會直接把新項目塞進去,而是會用包含新項目的數組新版本覆蓋原來的存儲。

2. Reducer

於是,我們的存儲是通過“減速器”(Reducer)更新的。這些基本上就是我們發送新版本狀態的機制。可能有點不知所云,我們詳細說明一下。假設我們有一個存儲對象,它的數組看起來像這樣:list: [{‘id: 1, text: ‘clean the house’}]。如果我們有一個將新項目添加到數組中的函數,那麼我們的減速器將向存儲解釋新版本的存儲具體是什麼樣子的。因此考慮這個list數組的情況,我們就會獲取list的內容,並通過…語法將其與要添加的新項目一起傳播到新的 list 數組中。因此,我們用來添加新項目的 reducer 應該是這個樣子的:list: […list, newItem]。所以前面我們說要爲存儲創建狀態的新副本,而不是將新項目推送到現有的存儲上,就是這個意思。

3. 動作

現在,爲了讓Reducer知道要放入哪些新數據,他們需要訪問負載(payload)。這個負載通過所謂"動作"(Action)的操作發送到減速器。就像我們創建的所有函數一樣,動作通常可以在應用的組件內通過 props 訪問。因爲這些動作位於我們的組件中,所以我們可以向它們傳遞參數——也就是負載。

理解上述內容後,我們就可以這樣理解 Redux 的工作機制了:應用可以訪問動作。這些動作會攜帶應用數據(通常也稱爲有效負載)。動作具有與減速器共享的類型。每當動作類型被觸發時,它就會拾取負載並通知存儲,告訴後者新版存儲應該是什麼樣的——這裏我們指的是數據對象在更新後應該是什麼樣子。

Redux 的理論模型還有其他內容,例如動作創建者和動作類型等,但是“To Do”應用不需要那些元素。

這裏的 Redux 設置可能是你學習它的一個很好的起點,當你更加熟悉 Redux 後,你可能會想要更進一步。考慮到這一點,儘管我前面說過 Redux 文檔可能讓人有點不知所措,但是當你要創建自己的設置時,應該好好看看那些文檔介紹的所有不同方法,作爲你靈感的源泉。

將 Redux 添加到 React 應用。

於是我們還是用 Create React App 創建 React 應用,方法都是一樣的。然後使用 yarn 或 npm 安裝兩個包:redux和react-redux,然後就可以開始了!還有一個稱爲redux-devtools-extension的開發依賴項,它可以確保你的 Redux 應用以你想要的方式工作。但它是可選的,如果你不想安裝也沒問題。

下面具體解釋下這些樣板是做什麼的

首先查看應用的根文件 main.js:

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import configureStore from "./redux/store/configureStore";
import App from "./App";

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

在這裏,我們有五個 import。前兩個是用於 React 的,我們不會再討論它們;而第五個導入只是我們的App組件。我們將重點關注第三和第四個導入。第三個導入是Provider,本質上是通向我們 Redux 存儲(前文所述)的網關。它的具體工作機制更復雜些,因爲我們需要選擇要訪問存儲的有哪些組件,稍後我們將討論其原理。

如你所見,我們用Provider組件包裝了App/組件。從上面的截圖中,你還會注意到,我們的Provider帶了一個存儲 prop,我們將store變量傳遞進這個 prop。第四個導入configure-Store實際上是我們已經導入的函數,然後將其輸出返回到store變量,如下:const store = configureStore();。

現在你可能已經猜到,這個configureStore基本上就是我們的存儲配置。這包括我們要傳遞的初始狀態。這是我們自己創建的文件,稍後我將詳細介紹。簡而言之,我們的 main.js 文件會導入存儲,並用它包裝根App組件,從而提供對它的訪問。

然而還需要更多樣板,所以我們往上走,看看根App組件中的其他代碼:

import React from "react";
import { connect } from "react-redux";
import appActions from "./redux/actions/appActions";
import ToDo from "./components/ToDo";
import "./App.css";

const App = (props) => {
  return <ToDo {...props} />;
};

const mapStateToProps = (state) => {
  return {
    list: state.appReducer.list
  };
};

const mapDispatchToProps = {
  ...appActions
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(App);

於是我們有另一個包含五個導入的文件。第一個是 React,第四個是 React 組件,第五個是 css 文件,因此我們不必再討論它們了。還記得前面提到的如何爲組件提供對存儲的訪問權限嗎?這就是第二個導入,connect的用途。

查看上面的代碼,你會看到,我們導出的不是App組件而是connect,這基本上是一種咖喱函數。咖喱函數本質上是返回另一個函數的函數。connect在這裏所做的是獲取mapState-ToProps和mapDispatchToProps的內容,然後獲取 App 組件,並將mapStateToProps和mapDispatch-ToProps的內容添加到其中,最後返回帶有新功能的App組件。大概就是這樣,但是mapStateToProps和mapDispatch-ToProps這些東西的內容是什麼呢?

其實mapStateToProps從存儲中獲取狀態,並將其向下傳遞爲連接的App組件的 prop。本例中我們給它賦予list鍵,因爲它遵循了我們在存儲內部指定的命名約定。不過我們不需要遵循此約定,而且可以隨意調用它——總之,只要我們要訪問這部分狀態,我們在應用中要引用的內容就是list。現在你知道了mapStateToProps是一個將state作爲參數的函數。本例中,state就是我們的store對象。作爲參考,如果我們將console.log(‘store’,store)放在mapStateToProps內,

const mapStateToProps = (state) => {
  return {
    list: state.appReducer.list
  };
};

輸出就會是:

考慮到這一點,我們本質上只是訪問store的某些部分,並通過 props 將這些部分附加到App中。本例中,我們可以從控制檯看到我們的狀態是一個名爲appReducer的對象,其中包含一個list數組。因此,我們通過mapStateTo-Props函數將其附加到App組件上,該函數返回一個具有list鍵和state.appReducer.list值的對象。看起來都很囉嗦還讓人頭暈,但希望這些內容能幫助你理解背後的邏輯。

那麼mapDispatchToProps呢?這裏就要提到 App.js 文件中的第三個導入,即appActions。這是我們創建的另一個文件,稍後將深入研究。現在只需知道mapDispatchToProps是一個普通對象,它將獲取我們將要創建的 動作 並將它們作爲 props 傳遞到我們連接的App組件中。用 Redux 術語來說,Dispatch 指的是對一個動作的分派,也就是我們正在執行一個函數的優美的說法。因此mapDispatchToProps就像 mapFunctionsToProps 或 mapActionsToProps。但是 React 文檔將其稱爲 mapDispatch-ToProps,因此我們在這裏遵循這條命名約定。

這裏要提醒一件事:在一個較大的典型 React 應用中,mapStateToProps函數在要返回的對象內部可能有許多不同的鍵 / 值對。這也可能來自 Redux 應用中 store 的許多不同的 reducer,因爲如果需要,你可以爲存儲提供訪問點。這同樣適用於mapDispatchToProps;雖然我們簡單的 To Do 應用只有一個文件來處理動作(appActions),但較大的應用可能有多個文件來處理針對應用各個部分的動作。你的mapDispatchToProps文件可能會從許多位置獲取動作,然後將它們作爲 props 傳遞到你的App組件。同樣,你需要自己決定該怎樣組織你的應用。

我們已經研究了從 Redux 溢出到根文件中的主要樣板,現在來看一下 Redux 文件夾中的情況,最後再談如何將它們全部整合到我們的 React 子組件內部(包括所有非根 App.js 組件的內容)。

Redux 文件夾

這裏有很多內容要講。首先再看一下應用的文件結構:

我們將按照上面截圖中的文件順序來討論。

動作

import { ADD_ITEM, DELETE_ITEM } from "../actionTypes";

const redux_add = (todo) => ({
  type: ADD_ITEM,
  payload: todo
});

const redux_delete = (id) => ({
  type: DELETE_ITEM,
  payload: id
});

const appActions = {
  redux_add,
  redux_delete
};

export default appActions;
actions/appActions.js

如前所述,appActions 就是我們導入到 App.js 中的文件。其中包含從應用中攜帶數據(也稱爲負載)的函數。對於這裏的 To Do 應用來說,我們需要三個功能:

1.保存輸入數據的功能;
2.添加項目的功能;
3.刪除項目的功能。

現在,第一個功能(保存輸入數據)實際上將在 ToDo 組件內部本地處理。我們也可以選擇用“Redux 方式”來處理,但我想強調的是並不是所有事情都必須通過 Redux 來做,如果你覺得使用 Redux 沒什麼意義,那就用不着它。本例中,我只想在組件級別處理輸入數據,同時在中央級別使用 Redux 維護實際的“待辦事項”列表。因此繼續介紹所需的其他兩個功能:添加和刪除項目。

這些功能只是獲取負載而已。爲了添加新的待辦事項,我們需要傳遞的負載就是新的 To Do 項目,因此我們的函數最終看起來像這樣:

const redux_add = (todo) => ({
  type: ADD_ITEM,
  payload: todo
})
appActions.js

在這裏,該函數有一個參數,我用它調用 todo,並返回一個具有type和payload的對象。我們將todo參數的值分配給payload鍵。你可能已經注意到了,這裏的 type 實際上是從 actionTypes 文件夾中導入的變量——稍後會具體介紹動作類型。

我們還有redux_delete函數,該函數將id作爲其負載,以便讓減速器知道要刪除哪個 To Do 項目。最後,我們有一個appActions對象,該對象將redux_add和redux_delete函數用作鍵和值。這也可以寫成:

const appActions = {
    redux_add: redux_add,
    redux_delete: redux_delete
}

你可能覺得這樣更好。另外要說的是,這裏的命名不是唯一的,例如appActions和函數前綴redux_,這只是我自己的命名約定。

動作類型

export const ADD_ITEM = "ADD_ITEM";
export const DELETE_ITEM = "DELETE_ITEM";
actionTypes/index.js

你可能還記得前文提到過的一種情況,那就是減速器和動作可以通過一種方式知道如何與彼此交互——這就是 類型(type) 的用途。我們的 減速器 也將訪問這些 操作類型。如你所見,這些只是變量,其名稱與其要分配的字符串相匹配。

這部分並不是必需的,你可以根據需要完全避免創建這個文件和模式。但這是 Redux 的最佳實踐,因爲它爲所有 動作類型 提供了一箇中心位置,從而減少了我們需要更新的位置數量。鑑於減速器也將使用這些位置,因此我們可以確信名稱總是正確的,畢竟它們都是來自於同一來源。下面來談 Reducer。

Reducer

這裏有兩個部分:appReducer 和 rootReducer。在較大的應用中,你可能有很多不同的減速器。這些都將被拉入你的 rootReducer 中。在本例中,考慮到我們的應用很小,我們可以只用一個減速器來處理。但我決定用兩個,因爲你可能會習慣這種做法。另外這裏的命名都是我的習慣,你可以給自己的減速器隨意取名。

下面來看看 appReducer:

import { ADD_ITEM, DELETE_ITEM } from "../actionTypes";

const initialState = {
  list: [{ id: 1, text: "clean the house" }, { id: 2, text: "buy milk" }]
};

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_ITEM:
      state = {
        list: [...state.list, action.payload]
      };
      return state;
    case DELETE_ITEM:
      state = {
        list: state.list.filter((todo) => todo.id !== action.payload)
      };
      return state;
    default:
      return state;
  }
}
reducers/appReducer.js

首先我們看到,我們正在導入之前 動作 用過的 動作類型。接下來的是initialState變量,它是狀態。這就是我們用來初始化存儲的方式,以便我們有一些初始狀態。如果你不需要任何初始狀態,則可以在自己的項目中用一個空對象——同樣,具體項目具體分析。

接下來是appReducer函數,它帶有兩個參數:第一個是state參數,這是我們開始的狀態。在本例中,我們使用默認參數將第一個參數默認爲initialState對象。這樣就不必再傳遞任何內容了。第二個參數是action。現在,每當觸發appActions.js文件中的一個函數時,就會觸發這個appReducer函數——稍後討論如何觸發這些函數,但現在我們只知道這些函數最終會在 ToDo.js 文件中結束。總之,每次觸發這些函數時,appReducer都會運行一系列switch語句,來查找與傳入的action.type匹配的語句。爲了瞭解被觸發的數據長什麼樣, 這裏console.log出我們的action,如下所示:

export default function appReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_ITEM:
      state = {
        list: [...state.list, action.payload]
      };
      return state;
    case DELETE_ITEM:
      state = {
        list: state.list.filter((todo) => todo.id !== action.payload)
      };
      return state;
    default:
      return state;
  }
}

現在的應用中,假設我們在輸入字段中輸入“take out the trash”並按 + 按鈕來創建新的 To Do 項目,就會在控制檯看到以下內容:

現在除了負載外,我們可以看到action還有“ADD_ITEM”的type。這與switch語句具有的ADD_ITEM變量匹配:

switch (action.type) {
    case ADD_ITEM:
      state = {
        list: [...state.list, action.payload]
      };
      return state;

當存在匹配項時,它將執行此操作,告訴存儲應如何設置其新狀態。在本例中,我們要告訴存儲,狀態現在應該等於一個list數組,其中包含之前的list數組的內容以及傳入的新payload,再看看控制檯的內容:

現在請記住,這個action帶有負載——這部分由我們在 appActions.js 中看到的動作處理。我們的 減速器 會根據action.type匹配的內容來選擇 動作 並處理。

現在看一下 rootReducer:

import { combineReducers } from "redux";
import appReducer from "./appReducer";

const rootReducer = combineReducers({
  appReducer
});

export default rootReducer;
reducers/index.js

第一個導入是combineReducers。這是一個 Redux 輔助函數,它收集了你的所有減速器並將它們變成一個對象,然後可以將其傳遞給store中的createStore函數,稍後具體介紹。第二個導入是我們先前創建和討論的appReducer文件。

如前所述,我們的應用非常簡單,因此實際上並不需要這個步驟。但爲了學習的目的,我決定保留這一步。

存儲

然後看一下 configureStore.js 文件:

import { createStore } from "redux";
import rootReducer from "../reducers";

export default function configureStore() {
  return createStore(rootReducer);
}

store/configureStore.js

這裏的第一個導入是createStore,它保存你應用的完整狀態。你只能擁有一個存儲。你可以有許多具有自己initialState的減速器。關鍵是要了解這裏的區別,儘管本質上你可以擁有許多提供某種形式狀態的 減速器,但是你只能有一個 存儲 從 減速器 中提取所有數據。

這裏的第二個導入是rootReducer,之前已經介紹過。你將看到創建了一個名爲configure-Store的簡單函數,該函數將createStore導入作爲函數返回,這個函數將rootReducer作爲其唯一參數。

同樣,這部分也可以跳過去,只需在根index.js文件中創建存儲即可。我之所以保留在這裏,是因爲你可能需要爲 存儲 做許多配置,從設置中間件到啓用其他 Redux 開發工具等。這種情況非常典型,但現在全介紹一遍太囉嗦,因此我從configureStore中移除了這個應用不需要的內容。

好的,現在我們已經在 Redux 文件夾中設置好了所有內容,並將 Redux 連接到了 index.js 文件和根 App.js 組件。下面該做什麼呢?

在應用中觸發 Redux 函數

現在快大功告成了。我們已經完成了所有設置,連接的組件可以通過mapStateToProps訪問存儲,還可以通過mapDispatchToProps作爲props訪問動作。我們訪問這些 props 的方法和 React 中的常見做法一樣,下面僅供參考:

const ToDo = (props) => {
  const { list, redux_add, redux_delete } = props;
ToDo.js

這三個 props 與我們傳入的相同:list包含state,而redux_add和redux_delete是添加和刪除函數。

然後,我們按需使用它們即可。在本例中,我用的函數與我之前的 vanilla React 應用中用過的一樣,區別只是這裏使用useState hook 通過某種setList()函數在本地更新狀態。我們用所需的負載調用redux_add或redux_delete函數。具體來看看:

const createNewToDoItem = () => {
    //validate todo
    if (!todo) {
      return alert("Please enter a todo!");
    }
    const newId = generateId();
    redux_add({ id: newId, text: todo });
    setTodo("");
  };
新增項目
const deleteItem = (todo) => {
    redux_delete(todo.id);
  };
刪除項目

看一下deleteItem函數,過一遍更新應用狀態的各個步驟。

redux_delete從我們要刪除的 To Do 項目中獲取 ID。

看一下 appActions.js 文件,會看到傳入的 ID 成爲payload的值:

const redux_delete = (id) => ({
  type: DELETE_ITEM,
  payload: id
});
appActions.js

然後我們在 appReducer.js 文件中看到,只要在switch語句中命中DELETE_ITEM類型,它就會返回狀態的新副本,該副本具有從負載中濾出的 ID:

case DELETE_ITEM:
      state = {
        list: state.list.filter((todo) => todo.id !== action.payload)
      };
      return state;
appReducer.js

隨着新狀態更新完畢,我們應用中的 UI 也會更新。

Redux 研究完成!

我們已經研究瞭如何將 Redux 添加到 React 項目、如何配置存儲、如何創建攜帶數據的動作以及如何創建用於更新存儲的減速器。我們還研究瞭如何將應用連接到 Redux,以便訪問所有的組件。我希望這些內容能幫到你,並讓你更好地理解 Redux 應用的模樣。

本文示例應用的 GitHub 鏈接:https://github.com/sunil-sandhu/redux-todo-2019

原文鏈接:
https://medium.com/javascript-in-plain-english/i-created-the-exact-same-app-with-react-and-redux-here-are-the-differences-6d8d5fb98222

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章