透過一個例子看看React是如何進行資料通訊的-第4篇

本專欄主要講講 React 中資料通訊的幾種方式:

透過一個例子看看React是如何進行資料通訊的-第1篇——props和事件回撥

透過一個例子看看React是如何進行資料通訊的-第2篇——Context物件

透過一個例子看看React是如何進行資料通訊的-第3篇——useContext

透過一個例子看看React是如何進行資料通訊的-第4篇——Redux

隨著

JavaScript

單頁應用開發日趨複雜,

JavaScript

需要管理越來越多的

state

。 比如說伺服器響應、快取資料、 UI 狀態、分頁器、loading 等等。而

Redux

就是

JavaScript

的一種狀態容器,能提供可預測化的狀態管理。

本身

Redux

是比較簡單的,在專案中使用需要先安裝依賴模組:

npm install redux ——save

然後先來看看他的幾個核心概念。

store

store

是一個物件,儲存了整個應用的

state

,相當於一個儲存資料的容器,且該容器在整個應用中有且只有一個。

建立方式如下:

// 。/index。jsimport { createStore } from ‘redux’import reducer from ‘。/reducers’let store = createStore(reducer)

createStore(reducer, [preloadedState], enhancer)

用來建立一個

store

來存放應用中所有的

state

,引數

reducer

是一個函式,引數

preloadedState

是初始時的

state

,會覆蓋

reducer

函式中的預設初始值,引數

enhancer

是一個高階函式,用於返回一個新的強化過的

store creator

想要獲取應用的

state

可以透過

getState()

方法獲取:

console。log(store。getState())

reducer

是一個函式,會根據不同的

action

來決定如何響應並返回一個新的

state

reducer

接收兩個引數,引數1是當前的

state

,引數2是

action

// 。/reducers/index。jslet initState = { // 初始 state  count: 0}function reducer(state = initState, action) {  switch(action。type) { // 根據傳入的 action 物件的 type 欄位來決定如何響應 state 的變化    case ‘INCREMENT’:      return { count: state。count + 1 }    case ‘DECREMENT’:      return { count: state。count - 1 }    default: // 無任何匹配時返回初始的 state      return state  }}

這裡需要注意的是

reducer

是一個純函式:

不要修改傳入的引數;

不要執行有副作用的操作,例如 API 請求、路由跳轉等;

不要呼叫非純函式,例如

Date。now()

Math。random()

等。

action

是一個描述事件的簡單物件,告訴我們發生了什麼事情,

reducer

函式會根據

action

去改變

state

,其中

action

必須擁有

type

屬性,用於決定要執行的行為,其他屬性可任意設定:

const incrementActionType = {  type: ‘INCREMENT’}const addCounterActionType = {  type: ‘ADD_COUNTER’,  step: 2}

一般我們會透過一個工廠方法來返回一個

action

物件:

function addCounter(num) {  return {    type: ‘ADD_COUNTER’,    step: num  }}export default addCounter

dispatch

action

描述發生了什麼事情,但是這個事情並不會主動發生,而是需要透過

dispatch

方法來觸發,它將

action

傳送到

reducer

函式中,然後

reducer

函式根據匹配規則進行狀態的更新。

import addCounter from ‘。/actions’store。dispatch(addCounter(1))

這是觸發

state

改變的惟一途徑。

subscribe

新增一個變化監聽器,每當

dispatch action

的時候都會執行的回撥。

store。subscribe(() => {  console。log(‘state 發生了改變: ’, store。getState()。count)})

一個完整的例子:

import { createStore } from ‘redux’// 生成 Action 的函式function incAction() {  return { type: ‘INCREMENT’ };}function decAction(num) {  return { type: ‘DECREMENT’, step: num };}// reducer 函式function reducer(state = { count: 0 }, action) {  switch (action。type) {    case ‘INCREMENT’:      return { count: state。count + 1 };    case ‘DECREMENT’:      return { count: state。count - action。step }; // 根據 action 其他欄位作為資料來源    default:      return state; // 無論如何都返回一個 state  }}const store = createStore(reducer); // 應用唯一 storeconsole。log( store。getState()。count ); // 初始的時候值:0// 監視當前 state 的變化store。subscribe(() => {  console。log(store。getState()。count);});store。dispatch(incAction()); // 1store。dispatch(incAction()); // 2store。dispatch(incAction()); // 3store。dispatch(decAction(2)); // 1

拆分 Reducer

隨著應用業務量的增加,

reducer

必定也會越來越大,就需要按模組來拆分

reducer

,有利於模組化開發,降低耦合度。透過

redux

提供的

combineReducers

方法可以將多個

reducer

進行組合。

// 。/countReducer。jsfunction countReducer(state = { count: 0 }, action) {  switch (action。type) {    case ‘INCREMENT’:      return { count: state。count + 1 };    case ‘DECREMENT’:      return { count: state。count - action。step };    default:      return state;  }}// 。/otherReducer。jsfunction otherReducer(state, action) {  // 。。。}import { combineReducers, createStore } from ‘redux’import countReducer from ‘。/countReducer’import otherReducer from ‘。/otherReducer’const mainReducer = combineReducers({  countReducer,  otherReducer})const store = createStore(mainReducer);console。log(store。getState()。countReducer); // 注意每個 reducer 根據它們的 key 來篩選出 stateconsole。log(store。getState()。otherReducer);

每個

reducer

只負責管理全域性

state

中它負責的一部分。每個

reducer

state

引數都不同,分別對應它管理的那部分

state

資料。

react-redux

上面我們已經認識了

redux

以及他的工作方式了,可以看出

redux

react

是沒有聯絡的。的確也是這樣,因為

redux

是獨立於其他框架而存在的,也就是說可以在任意框架中使用。而直接在

react

中使用

redux

不是很方便,這就需要用到

react-redux

這個庫來將兩者聯絡在一起。

安裝:

npm install redux react-redux ——save

Provider

使用

react-redux

提供的

元件來包裝整個應用,這樣所有的子元件都可以訪問

store

,而不必顯示的傳遞它。

import React from ‘react’import { render } from ‘react-dom’import { createStore } from ‘redux’import { Provider } from ‘react-redux’import reducer from ‘。/store/reducers’const store = createStore(reducer)render(  {/* 接收全域性 store 作為 props */}        ,  document。getElementById(‘root’))

connect

使用

react-redux

提供的

connect()

方法,可以使得 UI 元件能直接透過

props

拿到

store

中的資料以及修改資料的方法。

import React from ‘react’import { connect } from ‘react-redux’class Counter extends React。Component {  render() {    const { count, inc, dec } = this。props    return (      <>       -        {count}        +          )  }}function mapStateToProps(state) { // 將 store 中的 state 對映為 props  return {    count: state。count  }}function mapDispatchToProps(dispatch) { // 將修改 store 的方法對映為 props  return {    inc: () => dispatch({type: ‘INCREMENT’}),    dec: () => dispatch({type: ‘DECREMENT’})  }}export default connect(  mapStateToProps,  mapDispatchToProps)(Counter)

mapStateToProps(state [,ownProps])

用於建立

Counter

元件 跟

store。state

的對映關係,接收一個從

reducer

傳遞過來的

state

作為引數,定義從

state

轉換成 UI 元件

props

的規則。如果不傳遞,元件就不會監聽

store

的變化:

export default connect(  null,  mapDispatchToProps)(Counter)

mapDispatchToProps(dispatch)

用於建立

Counter

元件跟

store。dispatch

的對映關係,接收

Store

中的

dispatch

方法作為引數,也可以不用設定該引數。

export default connect(mapStateToProps)(Counter)

從上面的示例中可以看出

react-redux

中將元件分為了兩大部分:

容器元件:負責管理資料和業務邏輯,不負責 UI 的呈現,使用

Redux

的 API,如被

connect

包裝後的元件,

展示(UI)元件:只負責 UI 的呈現,不帶有任何業務邏輯,不使用任何

Redux

的 API,所有資料都有

props

提供,如

Counter

元件。

現在我們回到之前系列文章中的

List

元件看看,如何透過

redux

來解決跨元件通訊的問題。

透過一個例子看看React是如何進行資料通訊的-第4篇

先來看看專案結構:

├─src│  ├─store│  │  ├─index。js // store 建立│  │  ├─actionTypes│  │  │  ├─listActionType。js│  │  ├─reducers // 公共 reducers│  │  │  ├─index。js│  │  ├─actions // 公共 actions│  │  │  ├─listAction。js│  ├─components // 元件│  │  ├─List。jsx│  │  ├─Item。jsx│  │  ├─Add。jsx│  ├─views│  │  ├─App。jsx│  ├─index。js // 入口檔案├─package。json

store

相關程式碼:

// 。/src/store/index。js// 建立 storeimport { createStore } from ‘redux’import reducer from ‘。/reducers’let store = createStore(reducer)// 。/src/store/reducers/index。js// reducer 純函式,根據 action。type 欄位更新 stateimport * as actionType from ‘。。/actionTypes/listActionType’const initState = { // 初始 state  listData: [    {      id: 1,      title: ‘JavaScript’    }  ]}export default function listReducer(state = initState, action) {  switch (action。type) {    case actionType。ADDTYPE:      return {        。。。state,        listData: [          。。。state。listData,          {            id: Math。floor(Math。random() * 1000), // 模擬 id            title: action。title // action 物件其他欄位可用於傳遞資料          }        ]      }    case actionType。DELTYPE:      const list = state。listData。filter(item => item。id != action。id)      return {        。。。state,        listData: list      }    default:      return state    }}// 。/src/store/actions/listAction。js// action 工廠方法,呼叫函式,返回一個 action 物件import * as actionType from ‘。。/actionTypes/listActionType’export function addAction(title) {  return {    type: actionType。ADDTYPE,    title  }}export function delAction(id) {  return {    type: actionType。DELTYPE,    id  }}// 。/src/store/actionTypes/listActionType。js// 分離 action。type 欄位,因為是常量,所以變數名採用全大寫export const ADDTYPE = ‘ADD’export const DELTYPE = ‘DEL’

元件程式碼:

// 。/src/index。jsimport React from ‘react’import ReactDOM from ‘react-dom’import { Provider } from ‘react-redux’import store from ‘。/store’import App from ‘。/views/App’ReactDOM。render(        ,  document。getElementById(‘root’))// 。/src/views/App。jsximport List from ‘。。/components/List’function App() {  return (              

  );}export default App// 。/src/components/List。jsximport React, { Component } from ‘react’import { connect } from ‘react-redux’import Add from ‘。/Add’import Item from ‘。/Item’class List extends Component {  render() {    const { listData } = this。props    const items = listData。map(ele => )    return (      
                
          {items}        
      
    )  }}function mapStateToProps(state) { // 將 store 中的 state 對映為 props,這樣 List 元件可以透過 this。props。listData 訪問資料  return {    listData: state。listData  }}export default connect(  mapStateToProps)(List) // 將 List 元件包裝為容器元件// 。/src/components/Add。jsximport React, { Component } from ‘react’import { connect } from ‘react-redux’import { addAction } from ‘。。/store/actions/listAction’class Add extends Component {  state = {    title: ‘’  }  inputChange = (e) => {    this。setState({      title: e。target。value    })  }  render() {    const { title }  = this。state    const { add } = this。props    return (      
                 add(title) }>新增      
    )  }}function mapDispatchToProps(dispatch) { // 將修改 store 的方法對映為 Add 元件可以透過呼叫 this。props。add() 修改資料  return {    add: (title) => dispatch(addAction(title))  }}export default connect(  null,  mapDispatchToProps)(Add)// 。/src/components/Item。jsximport React, { Component } from ‘react’import { connect } from ‘react-redux’import { delAction } from ‘。。/store/actions/listAction’class Item extends Component {  render() {    console。log(this。props)    const { data, del } = this。props    return (      
        

{data。title}

         del(data。id)}>刪除      
    )  }}function mapDispatchToProps(dispatch) { // 將修改 store 的方法對映為 Item 元件可以透過呼叫 this。props。add() 修改資料  return {    del: (id) => dispatch(delAction(id))  }}export default connect(  null,  mapDispatchToProps)(Item)

寫到這裡,基本完成了一個簡單的

redux

專案,但還是差一點東西,比如說正常情況下,資料的獲取或者修改應該是基於伺服器上儲存的資料的,也就是說要加上 AJAX ,拿

List

元件來講,初始時應該從伺服器獲取資料,然後儲存到

store

中,看程式碼:

// 模擬請求function getData() {  const data = [    {      id: 1,      title: ‘JavaScript’    }  ]  return new Promise((resolve) => {    setTimeout(() => {   resolve(data) }, 1000)  })}// List。jsxclass List extends Component {  componentDidMount() {    getData()    。then(res => { // 非同步處理      this。props。getData(res)    })  }  render() {    const { listData } = this。props    const items = listData。map(ele => )    return (      

                
          {items}        
      
    )  }}function mapStateToProps(state) {  return {    listData: state。listData  }}function mapDispatchToProps(dispatch) {  return {    getData: (data) => dispatch(getAction(data))  }}export default connect(  mapStateToProps,  mapDispatchToProps)(List) // 將 List 元件包裝為容器元件

但是這樣做會增加元件的複雜程度,我們還可以把非同步的處理放在

connect

中:

// List。jsxclass List extends Component {  componentDidMount() {    this。props。getDataSync()  }  render() {    const { listData } = this。props    const items = listData。map(ele => )    return (      

                
          {items}        
      
    )  }}function mapStateToProps(state) {  return {    listData: state。listData  }}function mapDispatchToProps(dispatch) {  return {    getDataSync: () => {      getData()        。then(res => { // 非同步處理放在這裡          dispatch(getAction(res))        })    }  }}export default connect(  mapStateToProps,  mapDispatchToProps)(List)

雖然減少了元件的複雜度,但是也導致了

connect

不純淨了,所以如果可以

dispatch

一個函式,而不僅僅是一個

action

物件,將非同步放在

dispatch

階段處理就比較完美了,我們可以透過一箇中間件

redux-thunk

來實現

dispatch

可以傳入一個函式。

redux

store

僅支援同步資料流,而我們大部分的資料是需要發請求獲取的,而

redux-thunk

是一個比較流行的

redux

非同步

action

中介軟體,幫助我們統一了非同步和同步

action

的呼叫方式。

# 安裝npm install redux-thunk ——save

改造

store

// 。/src/store/index。jsimport { createStore, applyMiddleware } from ‘redux’import thunkMiddleware from ‘redux-thunk’import reducer from ‘。/reducers’let store = createStore(reducer, applyMiddleware(  thunkMiddleware, // 允許我們 dispatch() 函式))

改造

action

// 。/src/store/actions/listAction。jsimport * as actionType from ‘。。/actionTypes/listActionType’import { getData } from ‘。。/。。/api’export function getAction(data) { // 同步 action 工廠  return { // 這裡返回的是物件    type: actionType。GETTYPE,    data: data  }}export function getActionSync(data) { // 增加一個非同步操作  return function(dispatch) { // 注意這裡可以返回一個函式,這個函式有一個 dispatch 的引數    getData() // 非同步      。then(res => {        dispatch({          type: actionType。GETTYPE,          data: res        })      })  }}

List

元件改造:

import React, { Component } from ‘react’import { connect } from ‘react-redux’import Add from ‘。/Add’import Item from ‘。/Item’import { getActionSync } from ‘。。/store/actions/listAction’class List extends Component {  componentDidMount() {    this。props。getDataSync()  }  render() {    const { listData } = this。props    const items = listData。map(ele => )    return (      

                
          {items}        
      
    )  }}function mapStateToProps(state) {  return {    listData: state。listData  }}function mapDispatchToProps(dispatch) {  return {    getDataSync: () => dispatch(getActionSync()) // 新增 redux-thunk 後,可以 dispatch 一個函式  }}export default connect(  mapStateToProps,  mapDispatchToProps)(List)

這樣就清晰了很多,所有與

redux

有關的程式碼都放在了介面函式中,

connect

只是做了一箇中轉,在元件中只需要呼叫對應的方法即可。

useSelector(selector, [equalityFn])

react-redux

現在提供了一系列

hook APIs

(v7。1。0+)作為現在

connect()

高階元件的替代品。這些 APIs 允許你在不使用

connect()

包裹元件的情況下,訂閱

redux

store

,和 分發(

dispatch

actions

useSelector

函式的作用就是從

redux

中提取資料的。

import { useSelector } from ‘react-redux’export const Counter = () => {  // 透過傳入 selector 函式,從 Redux 的 store 中獲取 state  const count = useSelector(state => state。count)  return 

{count}
}

selector

函式與

connect

mapStateToProps

的引數是差不多一樣的。

selector

函式被呼叫時,將會被傳入

redux

store

的整個

state

,作為唯一的引數。每次函式元件渲染時,

selector

函式都會被呼叫。

預設情況下如果沒有指定

equalityFn

react-redux

會使用

===

來進行嚴格比較更新前後的資料。這樣就會存在效能問題,因為每次

Provider

value

發生變化的時候,子元件注入的

useSelector

鉤子也會被觸發執行,從而導致子元件的重新渲染。

useDispatch

這個

hook

返回

redux

store

的 分發(

dispatch

) 函式的引用。

import { useSelector, useDispatch } from ‘react-redux’export const Counter = () => {  const count = useSelector(state => state。count)  const dispatch = useDispatch()  return (    

      {value}       dispatch({ type: ‘INCREMENT’ })}>+    
  )}

這裡有一個建議在使用

dispatch

函式的回撥函式傳遞給子元件時,使用

useCallback

函式將回調函式記憶化,防止因為回撥函式引用的變化導致不必要的渲染。

最後我們使用

hook

來改造之前的元件:

// List。jsximport { useEffect } from ‘react’import { useSelector, useDispatch } from ‘react-redux’import Add from ‘。/Add’import Item from ‘。/Item’import { getActionSync } from ‘。。/store/actions/listAction’const List = () => {  const listData = useSelector(state => state。listReducer。listData)  const dispatch = useDispatch()  useEffect(() => {    dispatch(getActionSync())  }, [])  const items = listData。map(ele => )  return (    

            
        {items}      
    
  )}export default List// Item。jsximport { useDispatch } from ‘react-redux’import { delAction } from ‘。。/store/actions/listAction’const Item = (props) => {    const { data } = props    const dispatch = useDispatch()    return (      
        

{data。title}

         dispatch(delAction(data。id))}>刪除      
    )}export default Item// Add。jsximport { useState } from ‘react’import { useDispatch } from ‘react-redux’import { addAction } from ‘。。/store/actions/listAction’const Add = () => {  const [ title, setTitle ] = useState(‘’)  const dispatch = useDispatch()  const inputChange = (e) => {    setTitle(e。target。value)  }  return (    
             dispatch(addAction(title))}>新增    
  )}export default Add

到此,關於

redux

如何在

react

中使用就給大家講完了,關於

redux

其實就是一個狀態管理工具,是社群最流行的狀態管理工具,生態較好,支援

middleware

,提供了可預測的狀態管理,但需要寫大量的模板程式碼,且包體積在狀態管理工具中相對較大,適合大型專案。

【喜歡我的文章歡迎 轉發 點贊 與 關注,我會經常與大家分享前端的知識點的】