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

前言

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

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

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

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

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

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

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

file

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

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

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

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

Context 基于生產者-消費者模式,對應 React 中的三個概念: React.createContext 、 Provider、 Consumer 。通過調用 createContext 創(chuàng)建出一組 Provider 。Provider 作為數(shù)據(jù)的提供方,可以將數(shù)據(jù)下發(fā)給自身組件樹中的任意層級的 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工作流的簡單圖解:

file

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

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

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

但是 Context 的也是通過一個容器組件來管理狀態(tài)的,但是 ConsumerProvider 是一一對應的,在項目復雜度高的時候,可能會出現(xiàn)多個 ProviderConsumer ,甚至一個 Consumer 需要對應多個 Provider 的情況

當某個組件的業(yè)務邏輯變得非常復雜時,代碼會越寫越多,因為我們只能夠在組件內部去控制數(shù)據(jù)流,這樣導致 Model 和 View 都在 View 層,業(yè)務邏輯和 UI 實現(xiàn)都在一塊,難以維護

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

Redux

Redux 是 JS應用 的狀態(tài)容器,提供可預測的狀態(tài)管理

Redux 的三大原則

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

Redux工作流

file

不可變性( Immutability )

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

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

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

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

改變對象或者數(shù)組,內存中的引用地址尚未改變,但是內容已經改變

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

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 項目中使用 redux

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

Provider

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

Provider 做了些啥?

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

Connect

?connect 做了什么事情訥?

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

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

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

react-redux丐版實現(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; // 解構出store
  const state = store.getState(); // 拿到state
  //使用useReducer得到一個強制更新函數(shù)
  const [, forceComponentUpdateDispatch] = useReducer((count) => count + 1, 0);
  // 訂閱state的變化,當state變化的時候執(zhí)行回調
  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 的機會,在 middleware 中可以檢查每一個 action ,挑選出特定類型的 action 做對應操作

file

middleware示例

打印日志

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

監(jiān)控錯誤

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("錯誤報告: ", err);
  }
};
store.dispatch = catchMiddleware

catchMiddleware 中都寫死了,調用 loggerMiddleware ,loggerMiddleware 中寫死了 next(store.dispatch) ,需要靈活運用,讓 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("錯誤報告: ", err);
  }
};
/*loggerMiddleware 變成參數(shù)傳進去*/
store.dispatch = catchMiddleware(loggerMiddleware(next));

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

export const catchMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error("錯誤報告: ", 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));

每個 middleware 都需要接受 store 參數(shù),繼續(xù)優(yōu)化這個調用函數(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 來加載 middleware ,applyMiddleware 接受三個參數(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讓每個middleware獲得了middlewareAPI參數(shù)
    //形成一個chain匿名函數(shù)數(shù)組[f1,f2,f3...fn]
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    //dispatch=f1(f2(f3(store.dispatch))),把所有  的middleware串聯(lián)起來
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

applyMiddleware 符合洋蔥模型

file

總結

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

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容