本專欄主要講講 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 */}
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
來解決跨元件通訊的問題。
先來看看專案結構:
├─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(
{data。title}
寫到這裡,基本完成了一個簡單的
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 =>
但是這樣做會增加元件的複雜程度,我們還可以把非同步的處理放在
connect
中:
// List。jsxclass List extends Component { componentDidMount() { this。props。getDataSync() } render() { const { listData } = this。props const items = listData。map(ele =>
雖然減少了元件的複雜度,但是也導致了
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 =>
這樣就清晰了很多,所有與
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
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 (
這裡有一個建議在使用
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 =>
{data。title}
到此,關於
redux
如何在
react
中使用就給大家講完了,關於
redux
其實就是一個狀態管理工具,是社群最流行的狀態管理工具,生態較好,支援
middleware
,提供了可預測的狀態管理,但需要寫大量的模板程式碼,且包體積在狀態管理工具中相對較大,適合大型專案。
【喜歡我的文章歡迎 轉發 點贊 與 關注,我會經常與大家分享前端的知識點的】