一個小例子搞懂redux的套路

Redux的特色

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

Redux能作什麼?

隨着JavaScript單頁應用開發日趨複雜,管理不斷變化的state很是困難,Redux的出現就是爲了解決state裏的數據問題。在React中,數據在組件中是單向流動的,數據從一個方向父組件流向子組件(經過props),因爲這個特徵,兩個非父子關係的組件(或者稱做兄弟組件)之間的通訊就比較麻煩javascript

redux中各對象的說明

store

store是一個數據倉庫,一個應用中store是惟一的,它裏面封裝了state狀態,當用戶想訪問state的時候,只能經過store.getState來取得state對象,而取得的對象是一個store的快照,這樣就把store對象保護起來。html

action

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

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

actionCreator

若是每次派發動做時都寫上長長的action對象不是很方便,而actionCreator就是建立action對象的一個方法,調用這個方法就能返回一個action對象,用於簡化代碼。node

dispatch

dispatch是一個方法,它用於派發一個動做action,這是惟一的一個可以修改state的方法,它內部會調用reducer來調用不一樣的邏輯基於舊的state來更新出一個新的state。react

reducer

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

使用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

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

  • 可讓用戶添加待辦事項(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的源碼,其實也並不複雜。

相關文章
相關標籤/搜索