萌新 redux 入門

黑色瓶子發表於2018-12-25

我不萌,但新。
2018年要結束了,這一年,我。。。算了,不多說了。
想起上半年看的 react,到現在也沒怎麼用過,都快忘光了,趁現在有時間我來捋捋,首先從 redux 下手(與 react 有啥什麼關係????)

為什麼用 redux

我想學習 redux 的同學多少知道它的作用,官方定義 redux 是一個面向 JavaScript 應用的可預測狀態容器。如果用過 vuex 的,就能明白它是幹嘛的,它們作用是一樣的,主要用來管理共享狀態。不同於 vuex 與 vue 的關係,redux 與 react 是解耦的,雖然 redux 通常用在 react 中。至於為啥要用 redux,我覺得一張圖更好說明問題:

萌新 redux 入門

在react應用中,父子元件之間的資料傳遞比較容易,但是在複雜元件層級關係中,如圖左所示的那樣,資料的傳遞就顯得很麻煩了,而且容易混亂,這時如果有一個容器幫我們統一管理資料,並能共享到每個元件,那就很方便我們開發了,如圖右所示。而redux正是一個提供這樣功能的資料框架。

基本概念

概念這東西我不好說,我還是拿一張圖說明:

萌新 redux 入門

在 react 中使用 redux 時,某元件需要一個資料,那麼它就需要觸發一個 action,通過 dispatch 給 store,表明自己需要資料啦,action 是行為動作,它告訴 store 需要什麼樣的資料,它也是 store 資料的唯一來源。store 相當於一個管理員,它自己不直接處理資料,而是把當前的資料和收到的 action 告訴 reducer,讓 reducer 來處理資料,並將新資料返還給 store,再由 store 傳遞給元件,元件拿到的就是目標資料了。

初看時,可能有點繞,我們可以理解為一個在圖書館借書的場景。元件( Component)就相當於借書人,他要告訴管理員(Store),說:"我要借《時間簡史》",那麼說的這句話就可以理解為 Action,管理員(Store)聽到後,他不可能記得每本書的位置,於是就檢視記錄本看看書的資訊,那麼這個記錄本就相當於 Reducer,找到書的資訊後,管理員(Store)就把資訊告訴了借書人(Component)。可能比喻並不是那麼恰當,但表達的意思也差不多了。redux 就是圍繞 Action,Store,Reducer 這三點來展開的。

基本使用

為了方便起見,我直接用 create-react-app 腳手架建立一個專案來演示程式碼,安裝以及建立我就略過了。把專案中多餘的檔案刪除,src 目錄下只保留一個 index.js 檔案。

之前說過,redux 和 react 是解耦的,那麼我們先只在 index.js 檔案中來使用一下 redux,使用前別忘記安裝以下 redux

npm install redux
複製程式碼

index.js

// 引入 createStore 方法,用於建立 store
import { createStore } from "redux"

// 預設初始狀態
const defaultState = {
  a: 1
}

// 建立 reducer
function reducer(state = defaultState, action) {
  switch (action.type) {
    case "ADD":
      return Object.assign({}, state, {
        b: action.num
      });
    default:
      return state;
  }
}

// 建立 store
const store = createStore(reducer)

// 獲取 state
console.log('dispatch action 之前的資料:',store.getState())

// 派發 action
store.dispatch({
    type:"ADD",
    num:2
})

// 獲取更新後的 state
console.log('dispatch action 之後的資料:',store.getState())

複製程式碼

執行結果如圖:

萌新 redux 入門

以上就是一個redux的最基本的使用過程,我們來看下。

reducer

首先說一下 reducer,它用來處理資料,本質上是一個函式,有兩個引數 state 和 action,state 即是儲存的狀態,如果不給他賦初始值,那麼它就等於undefined,我在這裡給了它一個初始值 defaultState。action 即是行為,它是一個物件,必須有一個名為 type 的欄位來表示將要執行的動作,如程式碼裡的ADD就是我傳入的一個表示新增的行為,(注意type的值可任意,應儘量語意化)。除了 type 欄位外,你可以任意新增自己需要的欄位,比如這裡我傳裡一個 num 欄位,我需要將它新增到 state 中去。

reducer的注意事項:

  1. 不要直接修改 state。可以看到,我再程式碼裡使用 Object.assign() 新建了一個副本,再返回的。若不建立副本,redux 的所有操作都將指向記憶體中的同一個 state,所有的 state 都將被最後一次操作的結果所取代,我們將無法追溯 state 變更的歷史記錄。
  2. 在 default 情況下返回舊的 state。即沒有 action 時,一定要返回舊的 state。

action

前面說了 action 本質上是一個JavaScript中的物件,約定 action 內必須使用一個字串型別的 type 欄位來表示將要執行的動作。多數情況下,type 會被定義成字串常量。當應用規模越來越大時,建議使用單獨的模組或檔案來存放 action,如下:

// actionTypes.js
export const ADD = "ADD"
複製程式碼
import { ADD } from './actionTypes'

const action = {
    type: ADD,
    num: 2
}
複製程式碼

通過store.dispatch()方法將 action 傳到 store。

store

store將 action 和 reducer 聯絡起來,維持應用的 state,可以通過store.getState()來獲取 state,通過 store.dispatch() 來更新 state。我們通過引入 createStore 方法,並傳入 reducer 為第一個引數來建立 store。

在 react 中使用 redux

前面介紹了 redux 的基本使用方法,但是我們最終還是要使用在 react 中,現在我們就結合 react 來用下 redux。

首先,修改專案目錄,新增store目錄,用來存放 redux 相關檔案,而src/index.js檔案為元件入口:

萌新 redux 入門

我們就寫一個爛大街的計數器的例子,通過加減按鈕實現數字的增減。

首先是store中的程式碼:

actionTypes.js 用來統一存放 action 的 type 型別,並匯出

// 增
export const ADD = "ADD";
// 減
export const REDUCE = "REDUCE";
複製程式碼

reducer.js 建立 reducer 函式,並匯出

import { ADD, REDUCE } from "./actionTypes";

const defaultVal = 0;

function reducer(state = defaultVal, action) {
  switch (action.type) {
    case ADD:
      let newVal1 = state + 1;
      return newVal1;
    case REDUCE:
      let newVal2 = state - 1;
      return newVal2;
    default:
      return state;
  }
}

export default reducer;

複製程式碼

index.js 檔案建立 store,並匯出

import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

export default store;

複製程式碼

然後 src/index.js 檔案,是元件入口:

import React, { Component } from "react";
import ReactDOM from "react-dom";
import { ADD, REDUCE } from "./store/actionTypes";
import store from "./store";

class APP extends Component {
  render() {
    return (
      <div>
        <div>{store.getState()}</div>
        <div>
          <button onClick={this.handleReduce.bind(this)}>-</button>
          <button onClick={this.handleAdd.bind(this)}>+</button>
        </div>
      </div>
    );
  }

  // 執行減操作
  handleReduce() {
    store.dispatch({
      type: REDUCE
    });
  }

  // 執行加操作
  handleAdd() {
    store.dispatch({
      type: ADD
    });
  }
}

ReactDOM.render(<APP />, document.getElementById("root"));

複製程式碼

此時執行專案,在瀏覽器中實現加減操作。但是會發現並沒有用,怎麼點選都沒有預想的效果。其實這裡少了關鍵的一步,在 react 中使用不同於第一個例子裡那樣,第一個例子中我 dispatch 後會重新獲取 state,也就是store.getState(),但在 react 元件裡使用時,可以看到我只是獲取了一次 state,那麼我想要的效果是每當我 dispatch 後,這個 state 會自動更新。

redux 中提供了這樣的一個功能,就是store.subscribe(listener),它是 store 下的一個方法,會新增一個變化監聽器,每當 dispatch action 的時候就會執行。那麼有了監聽器,每次 dispatch 時我們需要執行啥呢?沒錯,我們給元件重新 render 下。修改ReactDOM.render這部分程式碼,如下:

function render() {
  ReactDOM.render(<APP />, document.getElementById("root"));
}
render();

store.subscribe(render);
複製程式碼

用一個函式包裹ReactDOM.render方法,並執行,這個函式其實就是 listener 了,把它傳給 subscribe,這樣每次 dispatch 後,就可以更新 state 了。

以上就是結合 react 和 redux 的一個簡單入門小例子,我寫的不一定清楚,但是程式碼敲一遍也能瞭解個大概。

補充

兩天後,我覺得還是要補充介紹下 redux 三大原則:

  1. 整個應用只能有一個 store,即const store = createStore(reducer) 這樣的建立 store,只能存在一個,保證單一資料來源;
  2. state 是隻讀的,唯一改變 state 的方法就是觸發 action。通過dispatch(action)來表達想要修改的意圖,所有的修改會被集中化處理;
  3. reducer 必須是純函式。前面也說過了,reducer 是接收先前的 state 和 action,並返回新的 state,而不是直接修改 state,這就是符合純函式的約束。

什麼是純函式?

  • 不能改寫引數,我們是通過引數返回新的數值的,同樣的輸入,得到同樣的輸出
  • 不能呼叫系統 I/O 的API
  • 不能呼叫Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結果

文中有寫的不對的地方,望有大佬指點。

相關文章