React中的數(shù)據(jù)流管理

前言

?? 為什么數(shù)據(jù)流管理重要?
React 的核心思想為:UI=render(data) ,data 就是所謂的數(shù)據(jù),render 是 React 提供的純函數(shù),所以 UI 展示完全由數(shù)據(jù)層決定。

在本文中,會(huì)簡(jiǎn)單介紹 React 中的數(shù)據(jù)流管理,從自身的 context 到三方庫(kù)的 redux 的相關(guān)概念,以及 redux 附屬內(nèi)容丐版實(shí)現(xiàn)。

在正文之前,先簡(jiǎn)單介紹數(shù)據(jù)狀態(tài)的概念。React 是利用可復(fù)用的組件來(lái)構(gòu)建界面,組件本質(zhì)上是有限狀態(tài)機(jī),能夠記住當(dāng)前組件的狀態(tài),根據(jù)不同的狀態(tài)變化做出相關(guān)的操作。在React中,把這種狀態(tài)定義為 state 。通過(guò)管理狀態(tài)來(lái)實(shí)現(xiàn)對(duì)組件的管理,當(dāng) state 發(fā)生改變時(shí),React 會(huì)自動(dòng)去執(zhí)行相應(yīng)的操作。

而數(shù)據(jù),它不僅指 server 層返回給前端的數(shù)據(jù),React 中的狀態(tài)也是一種數(shù)據(jù)。當(dāng)數(shù)據(jù)改變時(shí),我們需要改變狀態(tài)去引發(fā)界面的變更。

React自身的數(shù)據(jù)流方案

基于Props的單向數(shù)據(jù)流

React 是自上而下的單向數(shù)據(jù)流,容器組件&展示組件是最常見(jiàn)的 React 組件設(shè)計(jì)方案。容器組件負(fù)責(zé)處理復(fù)雜的業(yè)務(wù)邏輯和數(shù)據(jù),展示組件負(fù)責(zé)處理 UI 層。通常我們會(huì)把展示組件抽出來(lái)復(fù)用或者組件庫(kù)的封裝,容器組件自身通過(guò) state 來(lái)管理狀態(tài),setState 更新?tīng)顟B(tài),從而更新 UI ,通過(guò) props 將自身的 state 傳遞給展示組件實(shí)現(xiàn)通信

file

對(duì)于簡(jiǎn)單的通信,基于 props 串聯(lián)父子和兄弟組件是很靈活的。

但對(duì)于嵌套深數(shù)據(jù)流組件,A→B→C→D→E,A 的數(shù)據(jù)需要傳遞給 E 使用,那么我們需要在 B/C/D 的 props 都加上該數(shù)據(jù),導(dǎo)致最為中間組件的 B/C/D 來(lái)說(shuō)會(huì)引入一些不屬于自己的屬性

使用 Context API 維護(hù)全局狀態(tài)

Context API 是 React 官方提供的一種組件樹(shù)全局通信方式

Context 基于生產(chǎn)者-消費(fèi)者模式,對(duì)應(yīng) React 中的三個(gè)概念: React.createContext 、 Provider、 Consumer 。通過(guò)調(diào)用 createContext 創(chuàng)建出一組 ProviderProvider 作為數(shù)據(jù)的提供方,可以將數(shù)據(jù)下發(fā)給自身組件樹(shù)中的任意層級(jí)的 Consumer ,而 Consumer 不僅能夠讀取到 Provider 下發(fā)的數(shù)據(jù)還能讀取到這些數(shù)據(jù)后續(xù)的更新值

const defaultValue = {
  count: 0,
  increment: () => {}
};

const ValueContext = React.createContext(defaultValue);

<ValueContext.Provider value={this.state.contextState}>
  <div className="App">
    <div>Count: {count}</div>
    <ButtonContainer />
    <ValueContainer />
  </div>
</ValueContext.Provider>

<ValueContext.Consumer>
  {({ increment }) => (
    <button onClick={increment} className="button">increment</button>
  )}
</ValueContext.Consumer>

16.3之前的用法,16.3之后的createContext用法useContext用法

Context工作流的簡(jiǎn)單圖解:

file

在 v16.3 之前由于各種局限性不被推薦使用

  • 代碼不夠簡(jiǎn)單優(yōu)雅:生產(chǎn)者需要定義 childContextTypesgetChildContext ,消費(fèi)者需要定義 ChildTypes 才能夠訪問(wèn) this.context 訪問(wèn)到生產(chǎn)者提供的數(shù)據(jù)
  • 數(shù)據(jù)無(wú)法及時(shí)同步:類組件中可以使用 shouldComponentUpdate 返回 false 或者是 PureComponent ,后代組件都不會(huì)被更新,這違背了 Context 模式的設(shè)置,導(dǎo)致生產(chǎn)者和消費(fèi)者之間不能及時(shí)同步

在 v16.3 之后的版本中做了對(duì)應(yīng)的調(diào)整,即使組件的 shouldComponentUpdate 返回 false ,它仍然可以”穿透”組件繼續(xù)向后代組件進(jìn)行傳播,更改了聲明方式變得更加語(yǔ)義化,使得 Context 成為了一種可行的通信方案

但是 Context 的也是通過(guò)一個(gè)容器組件來(lái)管理狀態(tài)的,但是 ConsumerProvider 是一一對(duì)應(yīng)的,在項(xiàng)目復(fù)雜度高的時(shí)候,可能會(huì)出現(xiàn)多個(gè) ProviderConsumer ,甚至一個(gè) Consumer 需要對(duì)應(yīng)多個(gè) Provider 的情況

當(dāng)某個(gè)組件的業(yè)務(wù)邏輯變得非常復(fù)雜時(shí),代碼會(huì)越寫越多,因?yàn)槲覀冎荒軌蛟诮M件內(nèi)部去控制數(shù)據(jù)流,這樣導(dǎo)致 Model 和 View 都在 View 層,業(yè)務(wù)邏輯和 UI 實(shí)現(xiàn)都在一塊,難以維護(hù)

所以這個(gè)時(shí)候需要真正的數(shù)據(jù)流管理工具,從 UI 層完全抽離出來(lái),只負(fù)責(zé)管理數(shù)據(jù),讓 React 只專注于 View 層的繪制

Redux

Redux 是 JS應(yīng)用 的狀態(tài)容器,提供可預(yù)測(cè)的狀態(tài)管理

Redux 的三大原則

  • 單一數(shù)據(jù)源:整個(gè)應(yīng)用的 state 都存儲(chǔ)在一棵樹(shù)上,并且這棵狀態(tài)樹(shù)只存在于唯一的 store 中
  • state 是只讀的:對(duì) state 的修改只有觸發(fā) action
  • 用純函數(shù)執(zhí)行修改:reducer 根據(jù)舊狀態(tài)和傳進(jìn)來(lái)的 action 來(lái)生成一個(gè)新的 state (類似于 reduce 的思想,接受上一個(gè) state 和當(dāng)前項(xiàng) action ,計(jì)算出來(lái)一個(gè)新值)

Redux工作流

file

不可變性( Immutability )

mutable 意為可改變的,immutability 意為用不可改變的

在JS的對(duì)象( object )和數(shù)組( array )默認(rèn)都是 mutable,創(chuàng)建一個(gè)對(duì)象/數(shù)組都是可以改變內(nèi)容

const obj = { name: 'FBB', age: 20 };
obj.name = 'shuangxu';

const arr = [1,2,3];
arr[1] = 6;
arr.push('change');

改變對(duì)象或者數(shù)組,內(nèi)存中的引用地址尚未改變,但是內(nèi)容已經(jīng)改變

如果想用不可變的方式來(lái)更新,代碼必須復(fù)制原來(lái)的對(duì)象/數(shù)組,更新它的復(fù)制體

const obj = { info: { name: 'FBB', age: 20 }, phone: '177xxx' }
const cloneObj = { ...obj, info: { name: 'shuangxu' } }

//淺拷貝、深拷貝

Redux期望所有的狀態(tài)都采用不可變的方式。

react-redux

react-redux 是 Redux 提供的 react 綁定,輔助在 react 項(xiàng)目中使用 redux

它的 API 簡(jiǎn)單,包括一個(gè)組件 Provider 和一個(gè)高階函數(shù) connect

Provider

?為什么 Provider 只傳遞一個(gè) store ,被它包裹的組件都能夠訪問(wèn)到 store 的數(shù)據(jù)呢?

Provider 做了些啥?

  • 創(chuàng)建一個(gè) contextValue 包含 redux 傳入的 store 和根據(jù) store 創(chuàng)建出的 subscription ,發(fā)布訂閱均為 subscription 做的
  • 通過(guò) context 上下文把contextValue傳遞子組件

Connect

?connect 做了什么事情訥?

使用容器組件通過(guò) context 提供的 store ,并將 mapStateToPropsmapDispatchToProps 返回的 statedispatch 傳遞給 UI 組件

組件依賴 redux 的 state ,映射到容器組件的 props 中,state 改變時(shí)觸發(fā)容器組件的 props 的改變,觸發(fā)容器組件組件更新視圖

const enhancer = connect(mapStateToProps, mapDispatchToProps)
enhancer(Component)

react-redux丐版實(shí)現(xiàn)

Provider

export const Provider = (props) => {
  const { store, children, context } = props;
  const contextValue = { store };
  const Context = context || ReactReduxContext;
  return <Context.Provider value={contextValue}>{children}</Context.Provider>
};

connect

import { useContext, useReducer } from "react";
import { ReactReduxContext } from "./ReactReduxContext";

export const connect = (mapStateToProps, mapDispatchToProps) => (
  WrappedComponent
) => (props) => {
  const { ...wrapperProps } = props;
  const context = useContext(ReactReduxContext);
  const { store } = context; // 解構(gòu)出store
  const state = store.getState(); // 拿到state
  //使用useReducer得到一個(gè)強(qiáng)制更新函數(shù)
  const [, forceComponentUpdateDispatch] = useReducer((count) => count + 1, 0);
  // 訂閱state的變化,當(dāng)state變化的時(shí)候執(zhí)行回調(diào)
  store.subscribe(() => {
    forceComponentUpdateDispatch();
  });
  // 執(zhí)行mapStateToProps和mapDispatchToProps
  const stateProps = mapStateToProps?.(state);
  const dispatchProps = mapDispatchToProps?.(store.dispatch);
  // 組裝最終的props
  const actualChildProps = Object.assign(
    {},
    stateProps,
    dispatchProps,
    wrapperProps
  );
  return <WrappedComponent {...actualChildProps} />;
};

redux Middleware

“It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.” – Dan Abramov

middleware 提供分類處理 action 的機(jī)會(huì),在 middleware 中可以檢查每一個(gè) action ,挑選出特定類型的 action 做對(duì)應(yīng)操作

file

middleware示例

打印日志

store.dispatch = (action) => {
  console.log("this state", store.getState());
  console.log(action);
  next(action);
  console.log("next state", store.getState());
};

監(jiān)控錯(cuò)誤

store.dispatch = (action) => {
  try {
    next(action);
  } catch (err) {
    console.log("catch---", err);
  }
};

二者合二為一

store.dispatch = (action) => {
  try {
    console.log("this state", store.getState());
    console.log(action);
    next(action);
    console.log("next state", store.getState());
  } catch (err) {
    console.log("catch---", err);
  }
};

提取 loggerMiddleware/catchMiddleware

const loggerMiddleware = (action) => {
  console.log("this state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};
const catchMiddleware = (action) => {
  try {
    loggerMiddleware(action);
  } catch (err) {
    console.error("錯(cuò)誤報(bào)告: ", err);
  }
};
store.dispatch = catchMiddleware

catchMiddleware 中都寫死了,調(diào)用 loggerMiddleware ,loggerMiddleware 中寫死了 next(store.dispatch) ,需要靈活運(yùn)用,讓 middleware 接受 dispatch 參數(shù)

const loggerMiddleware = (next) => (action) => {
  console.log("this state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};
const catchMiddleware = (next) => (action) => {
  try {
    /*loggerMiddleware(action);*/
    next(action);
  } catch (err) {
    console.error("錯(cuò)誤報(bào)告: ", err);
  }
};
/*loggerMiddleware 變成參數(shù)傳進(jìn)去*/
store.dispatch = catchMiddleware(loggerMiddleware(next));

middleware中接受一個(gè)store,就能夠把上面的方法提取到單獨(dú)的函數(shù)文件中

export const catchMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error("錯(cuò)誤報(bào)告: ", err);
  }
};

export const loggerMiddleware = (store) => (next) => (action) => {
  console.log("this state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};

const logger = loggerMiddleware(store);
const exception = catchMiddleware(store);
store.dispatch = exception(logger(next));

每個(gè) middleware 都需要接受 store 參數(shù),繼續(xù)優(yōu)化這個(gè)調(diào)用函數(shù)

export const applyMiddleware = (middlewares) => {
  return (oldCreateStore) => {
    return (reducer, initState) => {
      //獲得老的store
      const store = oldCreateStore(reducer, initState);
      //[catch, logger]
      const chain = middlewares.map((middleware) => middleware(store));
      let oldDispatch = store.dispatch;
      chain
        .reverse()
        .forEach((middleware) => (oldDispatch = middleware(oldDispatch)));
      store.dispatch = oldDispatch;
      return store;
    };
  };
};

const newStore = applyMiddleware([catchMiddleware, loggerMiddleware])(
  createStore
)(rootReducer);

Redux 提供了 applyMiddleware 來(lái)加載 middleware ,applyMiddleware 接受三個(gè)參數(shù),middlewares 數(shù)組 / reduxcreateStore / reducer

export default function applyMiddleware(...middlewares) {
  return createStore => (reducer, ...args) => {
    //由createStore和reducer創(chuàng)建store
    const store = createStore(reducer, ...args) 
    let dispatch = store.dispatch
    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action, ...args) => dispatch(action, ...args)
    }
    //把getState/dispatch傳給middleware,
    //map讓每個(gè)middleware獲得了middlewareAPI參數(shù)
    //形成一個(gè)chain匿名函數(shù)數(shù)組[f1,f2,f3...fn]
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    //dispatch=f1(f2(f3(store.dispatch))),把所有  的middleware串聯(lián)起來(lái)
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

applyMiddleware 符合洋蔥模型

file

總結(jié)

本文意在講解 react 的數(shù)據(jù)流管理。從 react 本身的提供的數(shù)據(jù)流方式出發(fā)

  1. 基于 props 的單向數(shù)據(jù)流,串聯(lián)父子和兄弟組件非常靈活,但是對(duì)于嵌套過(guò)深的組件,會(huì)使得中間組件都加上不需要的 props 數(shù)據(jù)
  2. 使用 Context 維護(hù)全局狀態(tài),介紹了 v16.3 之前、v16.3之后的hooks ,不同版本 context 的使用,以及 v16.3 之前版本的 context 的弊端。
  3. 引入 redux ,第三方的狀態(tài)容器,以及 react-redux API ( Provider/connect )分析與丐版實(shí)現(xiàn),最后介紹了 redux 強(qiáng)大的中間件是如何重寫 dispatch 方法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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