一個小例子搞懂redux的套路

恍然小悟發表於2018-07-12

Redux的特點

  1. 統一的狀態管理,一個應用中只有一個倉庫(store)
  2. 倉庫中管理了一個狀態樹(statetree)
  3. 倉庫不能直接修改,修改只能通過派發器(dispatch)派發一個動作(action)
  4. 更新state的邏輯封裝到reducer中

Redux能做什麼?

隨著JavaScript單頁應用開發日趨複雜,管理不斷變化的state非常困難,Redux的出現就是為了解決state裡的資料問題。在React中,資料在元件中是單向流動的,資料從一個方向父元件流向子元件(通過props),由於這個特徵,兩個非父子關係的元件(或者稱作兄弟元件)之間的通訊就比較麻煩

一個小例子搞懂redux的套路

redux中各物件的說明

store

store是一個資料倉儲,一個應用中store是唯一的,它裡面封裝了state狀態,當使用者想訪問state的時候,只能通過store.getState來取得state物件,而取得的物件是一個store的快照,這樣就把store物件保護起來。

action

action描述了一個更新state的動作,它是一個物件,其中type屬性是必須有的,它指定了某動作和要修改的值:

{type: UPDATE_TITLE_COLOR, payload: 'green'}
複製程式碼

actionCreator

如果每次派發動作時都寫上長長的action物件不是很方便,而actionCreator就是建立action物件的一個方法,呼叫這個方法就能返回一個action物件,用於簡化程式碼。

dispatch

dispatch是一個方法,它用於派發一個動作action,這是唯一的一個能夠修改state的方法,它內部會呼叫reducer來呼叫不同的邏輯基於舊的state來更新出一個新的state。

reducer

reducer是更新state的核心,它裡面封裝了更新state的邏輯,reducer由外界提供(封裝業務邏輯,在createStore時傳入),並傳入舊state物件和action,將新值更新到舊的state物件上返回。

使用redux的流程

  1. 定義動作型別:
const INCREAMENT='INCREAMENT';
複製程式碼
  1. 定義專案的預設狀態,傳入reducer
let initState={...};
function reducer(state=initState,action){
    //...
}
複製程式碼
  1. 編寫reducer,實現更新state的具體邏輯
function reducer(state=initState,action){
    let newState;
    switch(action.type){
        //...
    }
    return newState;
}
複製程式碼
  1. 建立容器,傳入reducer
let store=createStore(reducer);
複製程式碼
  1. 訂閱需要的方法,當state改變會自動更新
store.subcribe(function(){});
複製程式碼
  1. 在需要更新state的地方呼叫dispatch即可
store.dispatch(/*某個action*/);
複製程式碼

可以看到通過以上幾個步驟,就可以使用redux,且不侷限於某種“框架”中,redux是一個設計思想,只要符合你的需求就可以使用redux。

在React中使用Redux

以下編寫一個待辦事項的小功能,描述如下:

  • 可以讓使用者新增待辦事項(todo)
  • 可以統計出還有多少項沒有完成
  • 使用者可以勾選某todo置為已完成
  • 可篩選檢視條件(顯示全部、顯示已完成、顯示未完成)

小專案的目錄結構:

專案根結點
┗━ components 存放元件
    ┗━ todo-header.js
    ┗━ todo-list.js
    ┗━ todo-footer.js
    ┗━ index.js
┗━ store 儲存redux的相關檔案
        ┗━ actions 定義action
            ┗━ action-type 定義動作型別
                ┗━ action-types.js
            ┗━ index.js
        ┗━ reducers 定義reducer
            ┗━ index.js
        ┗━ index.js 預設檔案用於匯出store
┗━ index.html 模版頁面
複製程式碼

以上4個功能我們使用redux結合react來實現。

元件拆分為3個:

  • TodoHeader 用於展示未辦數量和新增待辦
  • TodoList 按條件展示待辦項列表
  • TodoFooter 功能按鈕(顯示全部、未完成、已完成)

統計未完成的事項

此功能的核心就是把所有“未完成”的數量統計出來,在編寫redux程式時,首先定義好預設state,預設state是寫在reducer中的:

//定義預設狀態
let initState = {
  todos: [
    {
      id: parseInt(Math.random() * 10000000),
      isComplete: false,
      title: '學習redux'
    }, {
      id: parseInt(Math.random() * 10000000),
      isComplete: true,
      title: '學習react'
    }, {
      id: parseInt(Math.random() * 10000000),
      isComplete: false,
      title: '學習node'
    }
  ]
};
複製程式碼

在reducer目錄下建立一個index.js,由於這4個功能點過於簡單,不必拆分為多個reducer,因此所有的功能都在這一個index.js檔案中完成。這樣還可以減少combineReducer這個步驟。

以上定義了一個預設state物件,它裡面有3條資料,描述了待辦事項的內容。

由於目前沒有具體的功能邏輯,我們建立一個空的reducer:

function reducer(state=initState,action){
  return state;
}
export default reducer;
複製程式碼

可以看到,傳入了預設的initState,這樣就可以基於舊的state物件來作更新,每次reducer都會根據原state更新出一個新的state返回。

之後就可以建立倉庫(store),引用剛剛寫好的reducer,並把store返回給頂層元件使用:

import {createStore} from 'redux';
import reducer from './reducers';

let store = createStore(reducer);//傳入reducer
export default store;
複製程式碼

在store目錄下的index.js預設匯出store物件,方便元件引入。

在根元件中引入store物件,它是所有元件的容器,因此它要做所有元件的store提供者的角色,所以它的任務要把store提供給所有子元件使用,這就需要react-redux包提供的一個元件:Provider

Provider也是一個元件,它只有一個屬性:store,傳入建立好的store物件即可:

import {Provider} from 'react-redux';
import store from '../store';
//其它程式碼略...
ReactDOM.render(<Provider store={store}>
  <div>
    <TodoHeader/>
    <TodoList/>
    <TodoFooter/>
  </div>
</Provider>, document.querySelector('#root'));
複製程式碼

這樣就意味著Provider包裹的所有元件都可合法的取到store。

現在資料已經提供,還需要子元件來接收,同樣接收store資料react-redux包也為我們提供了一個方法:connect

connect這個方法非常奇妙,它的功能非常強大,它可以把倉庫中state資料注入到元件的屬性(this.props)中,這樣子元件就可以通過屬性的方式拿到倉庫中的資料。 首先定義一個頭元件,用於顯示未完成的數量:

import React from 'react';
import ReactDOM from 'react-dom';

class TodoHeader extends React.Component {
//程式碼略...
}
複製程式碼

下面使用connect方法將state資料注入到TodoHeader元件中:

import {connect} from 'react-redux';
let ConnectedTodoHeader = connect((state) => ({
  ...state
}))(TodoHeader);
複製程式碼

可以看到它的寫法很怪,connect是一個高階函式(函式返回函式),它的最終返回值是一個元件,這個元件(ConnectedTodoHeader)最終“連線”好了頂層元件Provider提供的store資料。

connect的第一個引數是一個函式,它的返回是一個物件,返回的物件會繫結到目標元件的屬性上,函式引數state就是store.getState的返回值,使用它就可以取到所有state上的資料,目前state就是todos的3條待辦事項

而高階函式傳入的引數就是要注入的元件,這裡是TodoHeader,這樣在TodoHeader元件中就可以通過this.props.todos取到待辦事項的資料。

這樣就可以編寫好我們的第一個統計功能,下面附上程式碼:

class TodoHeader extends React.Component {
  //取得未完成的todo數量
  getUnfinishedCount() {
    //this.props.todos就是從connect傳入的state資料
    return this.props.todos.filter((i) => {
      return i.isComplete === false;
    }).length;
  }
  render() {
    return (<div>
      <div>您有{this.getUnfinishedCount()}件事未完成</div>
    </div>);
  }
}

//匯出注入後的元件
export default connect((state) => ({
  ...state//此時的state就是todos:[...]資料
}))(TodoHeader);

複製程式碼

可以看到,通過connect取得state注入到元件屬性上,即可編寫邏輯完成功能。

新增待辦項

接下來完成新增待辦項的功能,使用者在一個文字框中輸入待辦項,把資料新增到倉庫中,並更新檢視。

由於有使用者的操作了,我們需要編寫動作(Action),Action需要一個具體的動作型別,我們在action-types.js中建立需要動作型別即可:

//新增待辦事項
export const ADD_TODO = 'ADD_TODO';
複製程式碼

可以看到它非常簡單,就定義了一個動作型別,也就是一個描述Action動作的指令,匯出它給reducer來使用。

接下來編寫ActionCreator,它是一個函式,只返回用剛剛這個指令生成的Action物件:

import {ADD_TODO} from './action-type/action-types';

let actions = {
  addTodo: function(payload) {
    return {type: ADD_TODO, payload};
  }
};

export default actions;//匯出ActionCreators
複製程式碼

可以看到引入了action-type,addTodo返回了一個形如{type:XXX, payload:XXX}的一個Action物件。這就是一個標準的Action物件的形式,第二個引數payload就是使用者傳入的引數。

注意在匯出時一定要將ActionCreator函式包到一個物件中返回,這樣redux內部會通過bindActionCreators將dispatch的功能封裝到每個函式中,這樣在connect連線時極大的方便了使用者的操作,稍候會看到。

下面編寫reducer,它裡面封裝了“新增待辦項”的邏輯:

import {ADD_TODO} from '../actions/action-type/action-types';
//部分程式碼略...
function reducer(state = initState, action) {
  let newState;
  switch (action.type) {
    case ADD_TODO:
      newState = {
        todos: [
          ...state.todos,
          action.payload
        ]
      };
      break;
    default:
      newState = state;
      break;
  }
  return newState;
}
複製程式碼

以上通過switch語句的一個分支,判斷動作型別是不是“新增待辦”這個功能(ADD_TODO),這樣在原state物件的基礎上追加這條資料即可。

注意,每次reducer都返回一個新的物件,不要直接在原state.todos.push這條資料,因為reducer是一個純函式。

...是ES6的寫法,意為展開運算子,它是將原state.todos的資料展開,並在後面新增一條新資料,相當於合併操作。

好了,到此處理資料的部分已經寫好,又到了注入元件的工作了,建立展示待辦的元件TodoList:

import React from 'react';
import {connect} from 'react-redux';
class TodoList extends React.Component {
//程式碼略...
}
export default connect((state) => ({
  ...state
}))(TodoList);
複製程式碼

再次通過connect方法將state資料注入到元件 (TodoList)的屬性上,讓元件內部可以通過this.props取得state資料。

下面編寫展示待辦項的功能:

class TodoList extends React.Component {
  getTodos() {
    return this.props.todos.map((todo, index) => {
      return (<li key={index}>
        <input type="checkbox" checked={todo.isComplete}/> {
          todo.isComplete
            ? <del>{todo.title}</del>
            : <span>{todo.title}</span>
        }
        <button type="button" data-id={todo.id}>刪除</button>
      </li>);
    });
  }
  render() {
    return (<div>
      <ul>
        {this.getTodos()}
      </ul>
    </div>);
  }
}
複製程式碼

在元件中定義一個getTodos方法用於迴圈所有待辦項,可以看到通過this.props.todos即可拿到connect傳入的資料,並在render中呼叫getTodos渲染即可。

現在可以初探整個小專案的邏輯,我們取資料不再是通過一層一層的元件傳遞了,而是所有的資料操作都交由redux來解決,元件只負責展示資料。

更改待辦項狀態

接下來實現更改一條待辦項的狀態,當使用者給一條待辦打勾就記為已完成,否則置為未完成。

還是一樣,新建一個action-type:

//更改待辦項的完成狀態
export const TOGGLE_COMPLETE = 'TOGGLE_COMPLETE';
複製程式碼

建立actionCreator,引入這個action-type:

let actions = {
  //更改完成狀態,此處payload傳id
  toggleComplete: function(payload) {
    return {type: TOGGLE_COMPLETE, payload};
  }
    //其它略...
};
複製程式碼

由於使用者勾選一條記錄,應傳入id作為唯一標識,因此這裡的payload引數就是待辦項的id。

payload並不是一定要叫payload可以更改變數名,如todoId,redux中管個這變數叫載荷,因此這裡使用payload。

同樣在reducer中再加一個swtich分支,判斷TOGGLE_COMPLETE:

function reducer(state = initState, action) {
  let newState;
  switch (action.type) {
    case TOGGLE_COMPLETE:
      newState = {
        //迴圈每一條待辦,把要修改的記錄更新
        todos: state.todos.map(item => {
          if (item.id == action.payload) {
            item.isComplete = !item.isComplete;
          }
          return item;
        })
      };
      break;
    //其它程式碼略...
    default:
      newState = state;
      break;
  }
  return newState;
}
複製程式碼

可以看到這次是修改某一條記錄的isComplete屬性,因此使用map函式迴圈,找到id為action.payload的那一條,修改isComplete的狀態。

仍要注意,不要使用slice函式去修改原state,一定要返回一個基於state更新後的新物件,map函式的執行結果就是返回一個新陣列,因此使用map符合這裡的需求。

接下來為元件的checkbox元素新增事件,當使用者勾選時,呼叫對應的Action toggleComplete動作即可完成邏輯:

//引入actionCreators
import actions from '../store/actions';
//其它 程式碼略...
class TodoList extends React.Component {
  todoChange = (event) => {
    //當onChange事件發生時,呼叫toggleComplete動作
    this.props.toggleComplete(event.target.value);
  }
  getTodos() {
    return this.props.todos.map((todo, index) => {
      return (<li key={index}>
        <input type="checkbox" value={todo.id} checked={todo.isComplete} onChange={this.todoChange}/> {
          todo.isComplete
            ? <del>{todo.title}</del>
            : <span>{todo.title}</span>
        }
        <button type="button" data-id={todo.id}>刪除</button>
      </li>);
    });
  }
  render() {
    //略...
  }
}
export default connect((state) => ({
  ...state
}), actions)(TodoList); //第二個引數傳入actionCreators
複製程式碼

這裡的connect函式傳入了第二個引數,它是一個actionCreator物件,同理由於元件中需要呼叫Action派發動作以實現某個邏輯,比如這裡就是元件需要更新待辦項的狀態,則“功能”也是由redux傳給元件的。

這樣元件裡的this.props就可以拿到actionCreator的方法,以呼叫邏輯: this.props.toggleComplete()

現在可以看到connect函式的強大之處,不管是資料state和功能actionCreators,都是由redux傳給需要呼叫的元件。redux在內部自動處理了更新元件、資料傳遞的工作,我們開發者不必再為元件之間的通訊花費精力了。

我們的今後的工作就是按照redux的架構定義好動作(Action)和reducer,也就是業務邏輯,而其它繁複的工作都由redux來完成。

刪除待辦項的功能類似,不再詳述。

篩選檢視條件

篩選檢視條件需要預先定義好3個狀態,即檢視全部(all)只檢視未完成(uncompleted)和檢視已完成(completed)。

因此,我們修改初始化的狀態,讓它預設為“檢視全部”:

//定義預設狀態
let initState = {
    //display用於控制待辦項列表的顯示
  display:'all', 
  todos: [
  //略...
  ]
};

複製程式碼

同樣的套路,建立action-type:

//更改顯示待辦項的狀態
export const CHANGE_DISPLAY = 'CHANGE_DISPLAY';
複製程式碼

建立actionCreator:

//部分程式碼略...
let actions = {
  //更改顯示待辦項的狀態,
  //payload為以下3個值(all,uncompleted,completed)
  changeDisplay: function(payload) {
    return {type: CHANGE_DISPLAY, payload};
  }
};
複製程式碼

為reducer增加CHANGE_DISPLAY的邏輯:

//部分程式碼略...
function reducer(state = initState, action) {
  let newState;
  switch (action.type) {
    case CHANGE_DISPLAY:
      newState = {
        display: action.payload,
        todos: [...state.todos]
      };
      break;
    default:
      newState = state;
      break;
  }
  return newState;
}
複製程式碼

在元件中,根據display條件過濾待辦項的資料即可,這裡抽出一個方法filterDisplay來實現:

class TodoList extends React.Component {
  //按display條件過濾資料
  filterDisplay() {
     return this.props.todos.filter(item => {
      switch (this.props.display) {
        case 'completed':
          return item.isComplete;
        case 'uncompleted':
          return !item.isComplete;
        case 'all':
        default:
          return true;
      }
    });
  }
  getTodos() {
    return this.filterDisplay().map((todo, index) => {
      //略...
    });
  }
  render() {
    //略...
  }
}
export default connect((state) => ({
  ...state
}), actions)(TodoList);
複製程式碼

以上還是由connect方法注入資料到元件,根據狀態的display條件過濾出符合條件的資料即可。

到此,全部的功能已實現。

執行效果:

一個小例子搞懂redux的套路

這個例子雖簡單卻完整的展示了redux的使用,真正專案開發時只要遵循redux的“套路”即可。

要需瞭解redux的更深層邏輯原理,就要讀redux的原始碼,其實也並不複雜。

相關文章