一步步帶你入門Redux管理數(shù)據(jù)

閑聊

?
最近忙里偷閑學習了react。由于之前一直都是使用vue做項目,所以學習react的時候覺得既熟悉又陌生。
?
熟悉是因為它和vue擁有許多相似的概念,包括都推崇組件化、都擁有’props’的概念、核心都是視圖層框架等等。雖然react不像vue擁有那么多豐富的API,但是在我看來,正因為react本身沒有過度的封裝,再加上react的社區(qū)非常成熟與活躍,才使得react的開發(fā)靈活多變,相比起來,我覺得react更適合大型項目的開發(fā),react的函數(shù)式編程也更容易實現(xiàn)前端自動化測試。
?
尤大自己也說過vue從一開始的定位就是盡可能的降低前端開發(fā)的門檻,讓更多的人能夠更快地上手開發(fā)。所以學習起來,vue更加圓滑,而react相對陡峭。兩者在我看來都是非常優(yōu)秀的框架,沒有高低之分,我們可以根據(jù)不同的開發(fā)情況選擇不同的開發(fā)工具。

前言

今天主要是想寫一下如何在react中管理數(shù)據(jù),所以在閱讀這篇文章之前,我默認你已經(jīng)可以自己搭建react項目,并可以看懂react的基本代碼。如果你沒有使用過react,但有過其他框架的使用經(jīng)驗,那么我認為這并不太影響你這篇文章的觀看體驗。
?

Redux

Redux=Reducer+Flux,F(xiàn)lux是Facebook推出的最原始的輔助React的數(shù)據(jù)層框架,但是它并不是那么的好用,所以有人把Flux做了一個升級,變成了Redux。

為什么要使用redux

請看下面這張圖


組件通信

假設底部綠色的組件要和最頂層的組件通信,那么綠色的組件需要層層把消息轉(zhuǎn)發(fā)給父級組件,直到傳到最頂層的組件,如果我們項目中的組件非常之多,組件之間又經(jīng)常需要共享傳值的話,那么使用react這種父子通信的方式,整個項目的開發(fā)就會變得非常冗余,也不易維護。
?
前面說過,react是一個視圖層框架(并不是什么問題都依靠react解決,react只解決數(shù)據(jù)和頁面渲染——也就是搭建視圖, 至于組件渲染交給別的數(shù)據(jù)層框架來做額外的支撐),所以我們需要一個數(shù)據(jù)層框架去協(xié)助react幫助數(shù)據(jù)管理,目前主流和react搭配的就是redux。
?
redux要求我們把數(shù)據(jù)都存放在一個名為store的公共存儲區(qū)域,我們把數(shù)據(jù)都存放在store中。如果想通過綠色的組件改變數(shù)據(jù)傳給其他組件,那么我們只需要操作store就可以了,接著其他灰色的組件會自動感知到變化,然后重新去store中取數(shù)據(jù),這樣我們?nèi)〉降臄?shù)據(jù),就是剛剛綠色組件所更改的數(shù)據(jù)。也就是說,redux間接地幫我們實現(xiàn)了組件通信的功能,讓我們的組件通信變得非常的輕松。
?

??但是我們要知道,redux不是只為react服務的,而是為JavaScript服務的狀態(tài)容器,react-redux才是專門為react服務的狀態(tài)管理插件,本篇文章主要講解redux。

redux 三大原則

1.單一數(shù)據(jù)源
store是唯一的。
整個應用的數(shù)據(jù)被儲存在一棵object tree(對象樹)中,并且這個 object tree 只存在于唯一一個 store 中。
?
2.state是只讀的
唯一改變 state 的方法就是觸發(fā) action,action 是一個用于描述已發(fā)生事件的普通對象。
?
3.使用純函數(shù)執(zhí)行修改
為了描述 action 如何改變 state tree ,你需要編寫 reducers。
reducer必須是純函數(shù): 純函數(shù)是指給定固定的輸入, 就一定會有固定的輸出, 且不會有任何副作用; 一旦一個函數(shù)有一個settimeout或者ajax或者new Date相關(guān)內(nèi)容的時候, 它就不是一個純函數(shù), 所以reducer里不可以有異步的操作。
??副作用: 例如對參數(shù)的修改就是副作用, 這個時候reducer也就不是一個純函數(shù)了

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

在學習redux前,我希望你可以了解Mutabilit(可變性)和Immutability(不變性)這兩個概念。
?
首先從字面上理解,「可變」意味著可以出現(xiàn)變化,可以變化,就意味著可能會出現(xiàn)一些問題或是bug。
?
「不可變」就代表某些數(shù)據(jù)是不可修變的,如果想要改變不可變的數(shù)據(jù),那么只能去復制舊的數(shù)據(jù),再產(chǎn)生新的數(shù)據(jù)來取代舊的數(shù)據(jù),我們永遠不要去修改舊的數(shù)據(jù)。
?
我這里不做過多的贅述,如果你對這塊有興趣,可以去自行查找一些文章了解,本文只需要你了解這個概念。

redux的工作流程

redux工作流程

reactComponents: 每一個頁面上的組件。
actionCreators:管理action的地方。
action:動作,它是 store 數(shù)據(jù)的唯一來源。一般來說你會通過 store.dispatch() 將 action 傳到 store,通常是一個對象。
store:存儲數(shù)據(jù)的公共區(qū)域,也可以理解為把action和reducers聯(lián)系到一起的對象。
reducers:處理不同的action類型,告訴store該給組件什么樣的數(shù)據(jù),然后store再把這個數(shù)據(jù)給到對應的組件。
?
這里你或許會看的有點蒙,我下面用代碼來解釋一下redux的工作流程。

安裝redux

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

redux代碼講解

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

todoList

基礎結(jié)構(gòu)-取值

先在剛剛搭建好的react項目中的src文件下建立一個store文件夾(你也可以建在任何的組件文件夾下),在store里分別創(chuàng)建一個index.js和reducer.js。
?
reducer.js

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

?
index.js

// 從redux引入createStore方法
import { createStore } from 'redux'; 
// 從剛剛創(chuàng)建的reducer.js引入reducer
import reducer from './reducer';
// 定義一個名為store的redux存儲區(qū),我們把reducer作為參數(shù)傳入createStore方法來構(gòu)造這個存儲區(qū),store里的數(shù)據(jù)只可以通過reducer來修改。
const store = createStore(reducer);

// 導出store
export default store;

?
創(chuàng)建子組件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組件是一個無狀態(tài)組件,沒有任何的邏輯操作,所有邏輯操作交由父組件執(zhí)行。
?
接著修改你的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的數(shù)據(jù),再賦值給this.state
    this.state = store.getState();
  }
  render() {
    return (
      <div>
        <input type="text"/>
        <button>提交</button>
        <List list={this.state.list}></List>
      </div>
    )
  }
}

此時運行出來應該是這樣


todoList

目錄結(jié)構(gòu)


目錄

修改store

此刻我們已經(jīng)可以取到store里的數(shù)據(jù)了,那么我們現(xiàn)在想在點擊提交的時候,list里新增一條數(shù)據(jù),并且實時地響應出來,應該怎么做呢。
?
修改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) 創(chuàng)建action
    const action = {
      type: 'change_input_value',
      value: e.target.value
    }
    // 2) 傳給store
    store.dispatch(action);
    // 3) store如果接收到了action, 會自動把之前的數(shù)據(jù)和action傳給reducer (這步store幫我們做了)
  }
  handleClick() {
    const action = {
      type: 'add_todo_item',
    }
    store.dispatch(action);
  }
}

?
然后修改我們的reducer

const defaultState = {
  inputValue: '',
  list: ['默認數(shù)據(jù)1', '默認數(shù)據(jù)2']
};
// 4) reducer拿到之前的數(shù)據(jù)和當前操作的信息后對數(shù)據(jù)進行處理,然后返回新的數(shù)據(jù)給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;
}

此時我們會發(fā)現(xiàn)在input框里輸入數(shù)據(jù)頁面是沒有反應的,點擊提交,頁面上也沒有發(fā)生任何變化,別急,我們先來打印一下store,這也是我們學redux時經(jīng)常容易犯的錯誤。
?
我們在handleClick方法的最后,用store.getState()方法來打印一下store的值
??注意是最后,store.dispatch(action)的后面

console.log(store.getState());
打印store

我們發(fā)現(xiàn)store里的數(shù)據(jù)已經(jīng)被改變了,list增加了1條數(shù)據(jù),inputValue也被清空了,這證明我們之前在reducer中編寫的代碼都生效了,但是都并沒有渲染在頁面上。現(xiàn)在頁面上input的value值是空值,是因為一開始inputValue的值就是空,而不是我們后來清空的。這一切都因為我們并沒有在組件中去監(jiān)聽更新store里的數(shù)據(jù),我們應該在頁面中監(jiān)聽store,當store發(fā)生變化時,實時更新我們的數(shù)據(jù)。

監(jiān)聽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) 監(jiān)聽store的變化
    // 訂閱store, 只要store發(fā)生改變, subscribe里的函數(shù)就會被自動執(zhí)行
    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變化的時候, 調(diào)用store.getState()方法從store中重新取一次數(shù)據(jù), 然后調(diào)用setState替換掉當前組件中的數(shù)據(jù), 這樣就會同步數(shù)據(jù)了
    this.setState(store.getState());
  }
}

??我們上面說過,不要直接更改state的值,所以我們每次修改時都創(chuàng)建了一個新的state,返回的也是全新的state。
不過,大量重復的代碼就是問題的源泉,我們在編寫代碼時,理應去減少出現(xiàn)bug的可能性。所以,當我們?nèi)粘i_發(fā)時,我推薦使用immutable.js或一些其他的第三方庫——我們在最初就把state生成immutable對象, 這樣可以百分百保證state不會被改變。

總結(jié)

拿剛剛的例子來說,我們首先把input的值和store中的inputValue關(guān)聯(lián)到了一起,如果你想修改input框的value值,就必須通過修改store中的inputValue實現(xiàn)。我們用onChange事件監(jiān)聽了input,在每次修改input中的值的時候,我們都創(chuàng)建了一個action,并把這個action派發(fā)給了store。
?
store接收到了這個action,會自動把這個action傳給reducer。reducer拿到這個action,開始對比action的type值,并進行相應的數(shù)據(jù)操作,之后返回了一個新的數(shù)據(jù)給store。我們在組件內(nèi)監(jiān)聽了store的變化,所以當reducer把值返回給了store,store更新了自己的數(shù)據(jù),我們的組件就會監(jiān)聽到剛剛store的變化,隨之更換組件內(nèi)store的數(shù)據(jù)。
?
input輸入流程:App→store→reducer→store→App檢測到store發(fā)生變化,更新數(shù)據(jù),渲染頁面
?
點擊提交流程:App→store→reducer→store→App.js檢測到store發(fā)生變化,更新數(shù)據(jù)→父組件App重新渲染觸發(fā)子組件List更新渲染
?

優(yōu)化

寫到這里,如果你只想了解該怎么使用redux,那么至此之前的代碼應該已經(jīng)足夠讓你上手去使用redux了。但是其實上面的代碼中還有很多可以優(yōu)化的地方,我沒有直接把優(yōu)化過后的代碼寫出來是怕不易于初學者閱讀學習,容易看暈。
?
比如說我們應該利用actionTypes統(tǒng)一常量, 預防因拼寫引發(fā)的bug,以及將action的創(chuàng)建放到actionCreators中統(tǒng)一進行管理。這樣做的優(yōu)點除了提高代碼的可維護性,還可以方便自動化測試。
?
在實際開發(fā)中,redux也應遵照組件化開發(fā),建議每個組件都應該擁有自己的store文件夾,src目錄下的store應僅僅作為各個組件內(nèi)store的集合。
?
在子組件List上,我們使用數(shù)組的index作為key值并不是一個好的做法。事實上我認為不到萬不得已的情況不要使用index作為key值。因為列表每一項的順序都可能會發(fā)生變化(比如說我們?nèi)绻麆h除list中的某一項時,list的順序就發(fā)生了變化,list中每一項的index值都發(fā)生了改變),react又是通過diff算法去渲染頁面的,diff算法通過key值去對比虛擬dom,如果key值全部發(fā)生改變,那虛擬dom便會全部更新,這明顯會降低我們的性能,所以說使用數(shù)組的index作為key值是下下策,有興趣的話可以去看看這篇文章深度解析使用索引作為 key 的負面影響。
?
因為diff算法(虛擬dom從頂層 層層比對)的原因,所以在父組件內(nèi)只要一改變inputValue的值,子組件就會重新渲染,即使我們并沒有修改list數(shù)據(jù)。這同樣會降低我們的性能,試想一下,如果你擁有非常多的子組件,父組件輸入任何一個字符都會導致所有子組件的重新渲染,這會消耗多少的性能呢?
為了解決這種多余性能的消耗,我們應該在子組件內(nèi)利用react內(nèi)置的生命周期函數(shù)shouldComponentUpdate去阻止子組件跟隨父組件去執(zhí)行無謂的render函數(shù),這樣就可以避免虛擬dom的比對,提升性能。
?

這篇文章到這里就全部結(jié)束了,本來想在一篇里把redux和react-redux都寫出來,但是怕太長了,所以下次找時間再寫react-redux吧。
?
如果你對這篇文章有任何疑問或補充,都可以在評論區(qū)給我留言討論。
如果你有興趣,還可以來我的博客看我的最新更新,我平時還會總結(jié)一些日語的小知識,喜歡日語的小伙伴也可以和我一起溝通討論。
?
順便一提最近看了排球少年的動漫,雖然比較冷門但是真的是一部不可多得的好作品,無論你喜不喜歡排球我覺得你看了這部動漫后都會愛上它的,強烈安利一波。
?
大家晚安啦。

烏野高校排球部

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容