一步步帶你入門Redux管理數據

閒聊

最近忙裏偷閒學習了react。因爲以前一直都是使用vue作項目,因此學習react的時候以爲既熟悉又陌生。
 
熟悉是由於它和vue擁有許多類似的概念,包括都推崇組件化、都擁有’props’的概念、核心都是視圖層框架等等。雖然react不像vue擁有那麼多豐富的API,可是在我看來,正由於react自己沒有過分的封裝,再加上react的社區很是成熟與活躍,才使得react的開發靈活多變,相比起來,我以爲react更適合大型項目的開發,react的函數式變成也更容易實現前端自動化測試。
 
尤大本身也說過vue從一開始的定位就是儘量的下降前端開發的門檻,讓更多的人可以更快地上手開發。因此學習起來,vue更加圓滑,而react相對陡峭。二者在我看來都是很是優秀的框架,沒有高低之分,咱們能夠根據不一樣的開發狀況選擇不一樣的開發工具。前端

前言

今天主要是想寫一下如何在react中管理數據。我會從搭建react項目開始,按部就班,若是你對這其中的某些過程已經很是瞭解,能夠在右側的目錄中跳過該章節。vue

Redux

Redux=Reducer+Flux,Flux是Facebook推出的最原始的輔助React的數據層框架,可是它並非那麼的好用,因此有人把Flux作了一個升級,變成了Redux。react

爲何要使用redux

請看下面這張圖
組件通訊ajax

假設底部綠色的組件要和最頂層的組件通訊,那麼綠色的組件須要層層把消息轉發給父級組件,直到傳到最頂層的組件,若是咱們項目中的組件很是之多,組件之間又常常須要共享傳值的話,那麼使用react這種父子通訊的方式,整個項目的開發就會變得很是冗餘,也不易維護
 
前面說過,react是一個視圖層框架(並非什麼問題都依靠react解決,react只解決數據和頁面渲染——也就是搭建視圖, 至於組件渲染交給別的數據層框架來作額外的支撐),因此咱們須要一個數據層框架去協助react幫助數據管理,目前主流和react搭配的就是redux
 
redux要求咱們把數據都存放在一個名爲store的公共存儲區域,咱們把數據都存放在store中。若是想經過綠色的組件改變數據傳給其餘組件,那麼咱們只須要操做store就能夠了,接着其餘灰色的組件會自動感知到變化,而後從新去store中取數據,這樣咱們取到的數據,就是剛剛綠色組件所更改的數據。也就是說,redux間接地幫咱們實現了組件通訊的功能,讓咱們的組件通訊變得很是的輕鬆。
 算法

❗️可是咱們要知道,redux不是隻爲react服務的,而是爲JavaScript服務的狀態容器,react-redux纔是專門爲react服務的狀態管理插件,本篇文章主要講解redux。

redux 三大原則

1.單一數據源
store是惟一的。
整個應用的數據被儲存在一棵object tree(對象樹)中,而且這個 object tree 只存在於惟一一個 store 中。
 
2.state是隻讀的
惟一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通對象。
 
3.使用純函數執行修改
爲了描述 action 如何改變 state tree ,你須要編寫 reducers。
reducer必須是純函數: 純函數是指給定固定的輸入, 就必定會有固定的輸出, 且不會有任何反作用; 一旦一個函數有一個settimeout或者ajax或者new Date相關內容的時候, 它就不是一個純函數, 因此reducer裏不能夠有異步的操做。
❗️反作用: 例如對參數的修改就是反作用, 這個時候reducer也就不是一個純函數了npm

Mutabilit(可變性) & Immutability(不變性)

在學習redux前,我但願你能夠了解Mutabilit(可變性)和Immutability(不變性)這兩個概念。
 
首先從字面上理解,「可變」意味着能夠出現變化,能夠變化,就意味着可能會出現一些問題或是bug。
 
「不可變」就表明某些數據是不可修變的,若是想要改變不可變的數據,那麼只能去複製舊的數據,再產生新的數據來取代舊的數據,咱們永遠不要去修改舊的數據。
 
我這裏不作過多的贅述,若是你對這塊有興趣,能夠去自行查找一些文章瞭解,本文只須要你瞭解這個概念。redux

redux的工做流程

redux工做流程
reactComponents: 每個頁面上的組件。
actionCreators:管理action的地方。
action:動做,它是 store 數據的惟一來源。通常來講你會經過 store.dispatch() 將 action 傳到 store,一般是一個對象。
store:存儲數據的公共區域,也能夠理解爲把action和reducers聯繫到一塊兒的對象。
reducers:處理不一樣的action類型,告訴store該給組件什麼樣的數據,而後store再把這個數據給到對應的組件。
 
這裏你或許會看的有點蒙,我下面用代碼來解釋一下redux的工做流程。數組

安裝redux

npm安裝 yarn安裝
npm install --save redux yarn add redux

redux代碼講解

我想實現一個todoList功能,當我點擊提交按鈕的時候,在input下面會增長我剛剛輸入的內容。
其中,input和button是父組件,下面的ul是子組件。
效果以下

todoList

基礎結構-取值

先在剛剛搭建好的react項目中的src文件下創建一個store文件夾(你也能夠建在任何的組件文件夾下),在store裏分別建立一個index.js和reducer.js。
 
reducer.js框架

// 定義初始數據defaultState,若是不給state設置一個初始數據,那麼最初state就是一個undefined。
// 這裏我已經爲todoList寫入了一個字符串inputValue和數組list。
const defaultState = {
  inputValue: '',
  list: ['默認數據1', '默認數據2']
};
export default (state = defaultState, action) => {
  // state指的是上一次存儲的數據, action是組件傳過來的內容
  return state;
}

 
index.jsdom

// 從redux引入createStore方法
import { createStore } from 'redux'; 
// 從剛剛建立的reducer.js引入reducer
import reducer from './reducer';
// 定義一個名爲store的redux存儲區,咱們把reducer做爲參數傳入createStore方法來構造這個存儲區,store裏的數據只能夠經過reducer來修改。
const store = createStore(reducer);

// 導出store
export default store;

 
建立子組件List.js

import React from 'react';

const List = (props) => {
    return (
      <div>
        <ul>
          {
            props.list.map((item, index) => {
              return <li key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    );
}

export default List;

❗️此處的List組件是一個無狀態組件,沒有任何的邏輯操做,全部邏輯操做交由父組件執行。
 
接着修改你的App.js (我這裏把App.js做爲父組件)

import React, { Component } from 'react'
import store from './store'
import List from './List'

export default class App extends Component {
  constructor(props) {
    super(props);
    // 用store的getState()方法取出store的數據,再賦值給this.state
    this.state = store.getState();
  }
  render() {
    return (
      <div>
        <input type="text"/>
        <button>提交</button>
        <List list={this.state.list}></List>
      </div>
    )
  }
}

此時運行出來應該是這樣
todoList
目錄結構
目錄

修改store

此刻咱們已經能夠取到store裏的數據了,那麼咱們如今想在點擊提交的時候,list裏新增一條數據,而且實時地響應出來,應該怎麼作呢。
 
修改App.js

import React, { Component } from 'react'
import store from './store';
import List from './List'
export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState();
    // 修改事件的this指向,不然this指向undefined
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  render() {
    return (
      <div>
        <input
          type="text"
          onChange={this.handleInputChange}
          value={this.state.inputValue}
        />
        <button onClick={this.handleClick}>提交</button>
        <List list={this.state.list}></List>
      </div>
    )
  }
  handleInputChange(e) {
    // 1) 建立action
    const action = {
      type: 'change_input_value',
      value: e.target.value
    }
    // 2) 傳給store
    store.dispatch(action);
    // 3) store若是接收到了action, 會自動把以前的數據和action傳給reducer (這步store幫咱們作了)
  }
  handleClick() {
    const action = {
      type: 'add_todo_item',
    }
    store.dispatch(action);
  }
}

 
而後修改咱們的reducer

const defaultState = {
  inputValue: '',
  list: ['默認數據1', '默認數據2']
};
// 4) reducer拿到以前的數據和當前操做的信息後對數據進行處理,而後返回新的數據給store
export default (state = defaultState, action) => {
  const newState = JSON.parse(JSON.stringify(state)); //深拷貝,由於reducer能夠接收state, 但毫不能修改state 因此要拷貝state
  switch (action.type) {
    case 'change_input_value':
      newState.inputValue = action.value;
      return newState; //return給了store
    case 'add_todo_item':
      newState.list.push(newState.inputValue);
      // 添加成功後清空inputValue
      newState.inputValue = '';
      return newState;
    default:
      break;
  }
  return state;
}

此時咱們會發如今input框裏輸入數據頁面是沒有反應的,點擊提交,頁面上也沒有發生任何變化,別急,咱們先來打印一下store,這也是咱們學redux時常常容易犯的錯誤。
 
咱們在handleClick方法的最後,用store.getState()方法來打印一下store的值
❗️注意是最後,store.dispatch(action)的後面

console.log(store.getState());

打印store
咱們發現store裏的數據已經被改變了,list增長了1條數據,inputValue也被清空了,這證實咱們以前在reducer中編寫的代碼都生效了,可是都並無渲染在頁面上。如今頁面上input的value值是空值,是由於一開始inputValue的值就是空,而不是咱們後來清空的。這一切都由於咱們並無在組件中去監聽更新store裏的數據,咱們應該在頁面中監聽store,當store發生變化時,實時更新咱們的數據。

監聽store

App.js最終代碼

import React, { Component } from 'react'
import store from './store';
import List from './List'
export default class App extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
    // 5) 監聽store的變化
    // 訂閱store, 只要store發生改變, subscribe裏的函數就會被自動執行
    this.handleStoreChange = this.handleStoreChange.bind(this);
    store.subscribe(this.handleStoreChange);
  }
  render() {
    return (
      <div>
        <input
          type="text"
          onChange={this.handleInputChange}
          value={this.state.inputValue}
        />
        <button onClick={this.handleClick}>提交</button>
        <List list={this.state.list}></List>
      </div>
    )
  }

  handleInputChange(e) {
    const action = {
      type: 'change_input_value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  handleClick() {
    const action = {
      type: 'add_todo_item',
    }
    store.dispatch(action);
  }

  handleStoreChange() {
    // 6) 當感知到store變化的時候, 調用store.getState()方法從store中從新取一次數據, 而後調用setState替換掉當前組件中的數據, 這樣就會同步數據了
    this.setState(store.getState());
  }
}
❗️咱們上面說過,不要直接更改state的值,因此咱們每次修改時都建立了一個新的state,返回的也是全新的state。
不過,大量重複的代碼就是問題的源泉,咱們在編寫代碼時,理應去減小出現bug的可能性。因此,當咱們平常開發時,我推薦使用immutable.js或一些其餘的第三方庫——咱們在最初就把state生成immutable對象, 這樣能夠百分百保證state不會被改變。

總結

拿剛剛的例子來講,咱們首先把input的值和store中的inputValue關聯到了一塊兒,若是你想修改input框的value值,就必須經過修改store中的inputValue實現。咱們用onChange事件監聽了input,在每次修改input中的值的時候,咱們都建立了一個action,並把這個action派發給了store。
 
store接收到了這個action,會自動把這個action傳給reducer。reducer拿到這個action,開始對比action的type值,並進行相應的數據操做,以後返回了一個新的數據給store。咱們在組件內監聽了store的變化,因此當reducer把值返回給了store,store更新了本身的數據,咱們的組件就會監聽到剛剛store的變化,隨之更換組件內store的數據。
 
input輸入流程:App→store→reducer→store→App檢測到store發生變化,更新數據,渲染頁面
 
點擊提交流程:App→store→reducer→store→App.js檢測到store發生變化,更新數據→父組件App從新渲染觸發子組件List更新渲染
 

優化

寫到這裏,若是你只想瞭解該怎麼使用redux,那麼至此以前的代碼應該已經足夠讓你上手去使用redux了。可是其實上面的代碼中還有不少能夠優化的地方,我沒有直接把優化事後的代碼寫出來是怕不易於初學者閱讀學習,容易看暈。
 
好比說咱們應該利用actionTypes統一常量, 預防因拼寫引起的bug,以及將action的建立放到actionCreators中統一進行管理。這樣作的優勢除了提升代碼的可維護性,還能夠方便自動化測試。
 
在實際開發中,redux也應遵守組件化開發,建議每一個組件都應該擁有本身的store文件夾,src目錄下的store應僅僅做爲各個組件內store的集合。
 
在子組件List上,咱們使用數組的index做爲key值並非一個好的作法。事實上我認爲不到萬不得已的狀況不要使用index做爲key值。由於列表每一項的順序均可能會發生變化(好比說咱們若是刪除list中的某一項時,list的順序就發生了變化,list中每一項的index值都發生了改變),react又是經過diff算法去渲染頁面的,diff算法經過key值去對比虛擬dom,若是key值所有發生改變,那虛擬dom便會所有更新,這明顯會下降咱們的性能,因此說使用數組的index做爲key值是下下策,有興趣的話能夠去看看這篇文章深度解析使用索引做爲 key 的負面影響
 
由於diff算法(虛擬dom從頂層 層層比對)的緣由,因此在父組件內只要一改變inputValue的值,子組件就會從新渲染,即便咱們並無修改list數據。這一樣會下降咱們的性能,試想一下,若是你擁有很是多的子組件,父組件輸入任何一個字符都會致使全部子組件的從新渲染,這會消耗多少的性能呢?
爲了解決這種多餘性能的消耗,咱們應該在子組件內利用react內置的生命週期函數shouldComponentUpdate去阻止子組件跟隨父組件去執行無謂的render函數,這樣就能夠避免虛擬dom的比對,提高性能。
 

這篇文章到這裏就所有結束了,原本想在一篇裏把redux和react-redux都寫出來,可是怕太長了,因此下次找時間再寫react-redux吧。
 
若是你對這篇文章有任何疑問或補充,均可以在評論區給我留言討論。
若是你有興趣,還能夠來個人 博客看個人最新更新,我平時還會總結一些日語的小知識,喜歡日語的小夥伴也能夠和我一塊兒溝通討論。
 
順便一提最近看了排球少年的動漫,雖然比較冷門可是真的是一部不可多得的好做品,不管你喜不喜歡排球我以爲你看了這部動漫後都會愛上它的,強烈安利一波。
 
你們晚安啦。
烏野高校排球部
相關文章
相關標籤/搜索