• 网络建设 之 React数据管理


    React作为一个用于构建用户界面的JavaScript库,很多人认为React仅仅只是一个UI 库,而不是一个前端框架,因为它在数据管理上是缺失的。在做一个小项目的时候,维护的数据量不多,管理/维护数据用useState/useRef就足够了;可是当项目变大,需要的数据量成百上千,然后就会发现:

    1. 全局变量到处都是。

    2. 在某些组件里定义的数据无法传递到其他组件里。

    3. 数据传来传去找不到定义位置,很难维护。

    因此这时候就需要数据管理了。

    最简单的数据管理

    就是把这些useState/useRef定义的数据放到根组件上,然后哪个子组件用,就用props传下去,这样没有其他概念浅显易懂,也起到了一定的数据管理的作用。但这样做的缺点就是这些数据需要在子组件一层层的传下去,代码要写很多,比较麻烦,如果不嫌麻烦的话,在大型项目里,这么做其实也没什么问题了。

    更进一步的数据管理,用useContext

    React的api,useContext,正是为了解决数据层层传递的问题而出现的,它可以看作是一个数据中心,所有需要管理的数据都在这里。

    它怎么用呢,首先新开一个文件context.js,在里用React.createContext()定义一个Context然后导出:

    1. //context.js
    2. import React from "react";
    3. export const Context = React.createContext();

    然后在根节点这里,用这个Context的Provider属性将整个根节点包裹住:

    1. // rootView.jsx
    2. import React from "react";
    3. import { Context } from "./context";
    4. export default function RootView() {
    5. const defaultValue = {a: 1, b: 'hello'};
    6. return <Context.Provider value={defaultValue}>
    7. <View class='root-view'>
    8. ...各种子组件...
    9. </View>
    10. </Context.Provider>;
    11. }

    这里的defaultValue就是我们数据中心的所有数据的初始化的默认值。

    然后在子组件里,不管是子组件还是孙组件还是孙孙组件,都不用再把 props 当传家宝传下去了,只需要在组件里像useState一样调用useContext,就能获取到数据中心的所有数据:

    1. //child.jsx
    2. import React, { useContext } from "react";
    3. import { Context } from "./context";
    4. export default function() {
    5. const state = useContext(Context);
    6. return <Text>{state.b}Text>
    7. }

    state就是数据中心的所有数据,可以理解为useState中的State,这样这个子组件显示的就是上面默认的初始化数据“hello”,但这还不够好用,因为目前还没有办法改变数据,那么我们接下来就需要对defaultValue做一些变动,把这些数据都用useState变成响应式的,然后再一股脑地传进Provider的value里:

    1. // rootView.jsx
    2. import React from "react";
    3. import { Context } from "./context";
    4. export default function RootView() {
    5. const [value, setValue] = useState({a: 1, b: 'hello'});
    6. return <Context.Provider value={{value, setValue}}>
    7. <View class='root-view'>
    8. ...各种子组件...
    9. </View>
    10. </Context.Provider>;
    11. }

    然后在子组件里这样调用:

    1. //child.jsx
    2. import React, { useContext } from "react";
    3. import { Context } from "./context";
    4. export default function() {
    5. const state = useContext(Context);
    6. useEffect(()=>{
    7. state.setValue({...state.value, b: 'world'});
    8. }, []);
    9. return <Text>{state.value.b}</Text>
    10. }

    然后,这个子组件显示的就是已经改动的数据“world”,关于Context还有一个比较重要的点是:当Context Provider的value发生变化时,他的所有调用useContext的子组件,都会重新渲染,这往往会造成比较严重的性能问题,在大型项目里百分百会出现。

    第一个问题是state改变,造成Provider标签下的整体渲染。Context.Provider说到底还是组件,也是用React.createElement()实现的,也按照组件基本法来办事,React.createElement()在每次props发生变动时,都会创建一个新对象,那么只要让props不发生变动就行了。我给Provider再包裹一层ProviderWrapper,然后在这个ProviderWrapper组件里去定义数据,这样,由于ProviderWrapper是不变的,那么在RootView组件里没有任何状态改变,子组件也用不着重复渲染了。

    1. const ProviderWrapper = ({ children }) => {
    2. const [value, setValue] =useState(defaultValue);
    3. return (
    4. <Context.Provider value={{ value, setValue }}>
    5. {children}
    6. </Context.Provider>
    7. );
    8. };
    9. export default function RootView() {
    10. return <ProviderWrapper>
    11. <View class='root-view'>
    12. ...各种子组件...
    13. </View>
    14. </ProviderWrapper>;
    15. }

    这样,babel在编译的时候,标签转译成React.createElement()的时候,只是在RootView组件里完成转译,React.createElement()执行完的节点数据将通过props.children传入ProviderWrapper,在ProviderWrapper内部就没有重复的React.createElement(),这样就避免了整体的重复渲染。

    二是上述的所有调用useContext的子组件的局部重复渲染。即便在某一个子组件中只是使用了setState,并没有使用state,但是当state变动时,这个子组件仍然会重复渲染,因为仅仅是调用了useContext,但理论上来说是不需要重复渲染的。那解决办法是什么呢?解决办法就是将state和setState分别用不同的Provider传入,这样一个组件仅仅只是调用setState的话,就不会被state的变动影响而重复渲染:

    1. const ProviderWrapper = ({ children }) => {
    2. const [value, setValue] =useState(defaultValue);
    3. return (
    4. <SetValueContext.Provider value={{ setValue }}>
    5. <ValueContext.Provider value={{ value }}>
    6. {children}
    7. </Context.Provider>
    8. </Context.Provider>
    9. );
    10. };

    其中SetValueContext和ValueContext是两个毫不相干的有React.createContext()产生的对象,仅仅只是用来区分开state和setState,这样在子组件里,如果只想调用setState,那么就通过React.useContext()引入SetValueContext即可,子组件就不会因state变动而重复渲染。

    这样基本上就差不多了,难懂的代码多了一些,但冗余的代码少了不少。概念越多就能解决的更多的问题,现在又出现了一个问题,state里有很多数据,一些子组件引用了React.useContext(),但是对state里的一些数据是不关心用不到的,但这些数据在发生变动的时候,这些子组件也会重复渲染,说白了,就是state细粒度不够的问题,但是本着尽可能消除重复渲染的思想,我们把state根据数据种类进行拆分成多个state,这样每个子组件调用对自己有用的state,这样就减少了重复渲染:

    1. const ProviderWrappers = ({ children }) => (
    2. <LoginProviderWrapper>
    3. <SignupProviderWrapper>
    4. <MainPageProviderWrapper>
    5. <MenuProviderWrapper>
    6. {children}
    7. </MenuProviderWrapper>
    8. </MainPageProviderWrapper>
    9. </SignupProviderWrapper>
    10. </LoginProviderWrapper>
    11. );
    12. export default function RootView() {
    13. return <ProviderWrappers>
    14. <View class='root-view'>
    15. ...各种子组件...
    16. </View>
    17. </ProviderWrappers>;
    18. }

    等一下,代码怎么变冗余了?我们最初的目的是什么?消除冗余,我们为了消除一种冗余,带来了另一种冗余,这是不可接受的,所以还得接着改,当前情况是,由于state被拆分,造成出现了很多ProviderWrapper支持不同的state和setState,那么我们需要对这些ProviderWrapper进行某种程度上的组合,至少我们可以用一个for循环去组合这些ProviderWrapper:

    1. // RootView.tsx
    2. function composeProviderWrappers(ProviderWrappers) {
    3. const element;
    4. for(ProviderWrapper of ProviderWrappers) {
    5. element = <ProviderWrapper>{element}</ProviderWrapper>
    6. }
    7. return element;
    8. }
    9. export default function RootView() {
    10. const ComposeProviderWrappers = composeProviderWrappers([LoginProviderWrapper, SignupProviderWrapper, MainPageProviderWrapper, MenuProviderWrapper]);
    11. return <ComposeProviderWrappers>
    12. <View class='root-view'>
    13. ...各种子组件...
    14. </View>
    15. </ComposeProviderWrappers>;
    16. }

    这个优化意义不大,并没有减少多少冗余代码,但是说实话,我们现在已经走歪了,而导致我们走歪的罪魁祸首,就是React.useContext()的性能问题:只要调用React.useContext()的组件,当state变动的时候,全部都会重新渲染。回到最开始说的,React相对于Framwork,其实它更类似于一个UI库,用React本身的功能勉强实现数据管理,代价就是有很多坑,毕竟使用一些第三方数据管理库例如Redux,zustand之类的,既能实现React.useContext()的功能,又能避免React.useContext()的问题,何乐而不为呢?下面就来介绍一些第三方数据管理库:

    Redux

    Redux可以说是最正统的React数据管理工具,Redux的用法与React.useContext()类似,但没有React.useContext()的缺点,只有组件在使用到变动的数据的时候,这个组件才会重新渲染,如果你在因使用React.useContext()导致的无限渲染大卡关时,不妨试试Redux。

    Redux只有2KB,Redux Toolkit是官方推荐的编写 Redux 逻辑的方法,使编写 Redux 更加容易。安装方式如下:

    1. # NPM
    2. npm install @reduxjs/toolkit redux
    3. # Yarn
    4. yarn add @reduxjs/toolkit redux

    使用时,首先像React.createContext()一样,使用configureStore导出一个实例:

    1. import { configureStore } from '@reduxjs/toolkit'
    2. export default configureStore({
    3. reducer: {}
    4. })

    然后用react-redux提供的Provider标签,将整个根节点包裹起来,唯一的区别就是,我们再也不用考虑担心性能问题了,这里不会有的:

    1. import React from 'react'
    2. import ReactDOM from 'react-dom'
    3. import './index.css'
    4. import App from './App'
    5. import store from './app/store'
    6. import { Provider } from 'react-redux'
    7. export default function RootView() {
    8. return <Provider store={store}>
    9. <View class='root-view'>
    10. ...各种子组件...
    11. View>
    12. Provider>;
    13. }

    然后不一样的来了,创建slice:

    1. import { createSlice } from '@reduxjs/toolkit'
    2. export const counterSlice = createSlice({
    3. name: 'counter',
    4. initialState: {
    5. value: 0
    6. },
    7. reducers: {
    8. increment: state => {
    9. // Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
    10. // 并不是真正的改变状态值,因为它使用了 Immer 库
    11. // 可以检测到“草稿状态“ 的变化并且基于这些变化生产全新的
    12. // 不可变的状态
    13. state.value += 1
    14. },
    15. decrement: state => {
    16. state.value -= 1
    17. },
    18. incrementByAmount: (state, action) => {
    19. state.value += action.payload
    20. }
    21. }
    22. })
    23. // 每个 case reducer 函数会生成对应的 Action creators
    24. export const { increment, decrement, incrementByAmount } = counterSlice.actions
    25. export default counterSlice.reducer

    这里的createSlice实际上可以考虑为创建state和setState,reducers就是setState。然后将 Slice Reducers 添加到 Store 中:

    1. import { configureStore } from '@reduxjs/toolkit'
    2. import counterReducer from '../features/counter/counterSlice'
    3. export default configureStore({
    4. reducer: {
    5. counter: counterReducer
    6. }
    7. })

    最后就是使用了,在 React 组件中使用 Redux 状态和操作:

    1. import React from 'react'
    2. import { useSelector, useDispatch } from 'react-redux'
    3. import { decrement, increment } from './counterSlice'
    4. import styles from './Counter.module.css'
    5. export function Counter() {
    6. const count = useSelector(state => state.counter.value)
    7. const dispatch = useDispatch()
    8. return (
    9. <div>
    10. <div>
    11. <button
    12. aria-label="Increment value"
    13. onClick={() => dispatch(increment())}
    14. >
    15. Increment
    16. </button>
    17. <span>{count}</span>
    18. <button
    19. aria-label="Decrement value"
    20. onClick={() => dispatch(decrement())}
    21. >
    22. Decrement
    23. </button>
    24. </div>
    25. </div>
    26. )
    27. }

    虽然起的名字不同,但是通过上述的React.useContext()的学习,基本上也是能一一对应的,最重要的是,这里不会再有性能问题了。

    zustand

    "Zustand" 只是德语的"state",一个轻量,现代的状态管理库,它的好处就是更简单。

    安装:

    npm install zustand

    然后老生常谈的定义一个实例:

    1. const useStore = create(set => ({
    2. votes: 0,
    3. addVotes: () => set(state => ({ votes: state.votes + 1 })),
    4. subtractVotes: () => set(state => ({ votes: state.votes - 1 })),
    5. }));

    然后,就可以使用了,这个真的比较方便:

    1. function App() {
    2. const addVotes = useStore(state => state.addVotes);
    3. const subtractVotes = useStore(state => state.subtractVotes);
    4. return <div className="App">
    5. <h1>{getVotes} people have cast their votes</h1>
    6. <button onClick={addVotes}>Cast a vote</button>
    7. <button onClick={subtractVotes}>Delete a vote</button>
    8. </div>
    9. }

    Rematch

    Rematch在Redux的基础上构建并减少了样板代码和执行了一些最佳实践。Redux对于初学者来说简直就是噩梦,他仿佛不是一个状态管理工具,而是一个涉及了众多概念的状态管理模型。要想搞明白Redux如何使用,就要先了解10个以上名词的含义;这还只是Redux的主流程使用中涉及到的名词。Redux的主流程里充斥了各种各样的概念,比如,Dispatch、Reducer、CreateStore、ApplyMiddleware、Compose、CombineReducers、Action、ActionCreator、Action Type、Action Payload、BindActionCreators...Rematch将这些概念进行了整合,提出了一个更简洁的状态管理模型;

    安装:

    npm install @rematch/core react-redux

    首先,定义一个实例:

    1. import { init } from "@rematch/core";
    2. // 定义一个model,包含了之前redux中的一些内容
    3. // 拥有对应的state和reducers
    4. //model
    5. const count = {
    6. state: 0,
    7. reducers: {
    8. upBy: (state, payload) => state + payload,
    9. },
    10. };
    11. // 使用init初始化
    12. // 相当于Redux中的store
    13. init({
    14. models: { count },
    15. });

    然后,就可以使用了:

    1. import { connect } from "react-redux";
    2. // Component
    3. //count内容赋值给count
    4. const mapStateToProps = (state) => ({
    5. count: state.count,
    6. });
    7. // 将指定动作传输给组件
    8. const mapDispatchToProps = (dispatch) => ({
    9. countUpBy: dispatch.count.upBy,
    10. });
    11. connect(mapStateToProps, mapDispatchToProps)(Component);
    12. // connect倒是没有怎么变

    jotai,recoil,redux,rematch,zustand,Reducer,react数据管理的哲学

  • 相关阅读:
    ESP8266-Arduino编程实例-ML8511紫外线(UV)传感器驱动
    浅谈MyBatis中遇到的问题~
    uniapp缓存对象数组
    UDS知识整理(六):通讯控制——0x28服务
    android中集成ffmpeg
    git--修改用户名和邮箱的方法(全局修改和局部修改)
    利用IDEA软件 创建springboot项目 整合MyBatis框架
    Paper reading:Fine-Grained Head Pose Estimation Without Keypoints (CVPR2018)
    快速搭建 SpringCloud Alibaba Nacos 配置中心!
    小程序云开发笔记一
  • 原文地址:https://blog.csdn.net/HeroIsUseless/article/details/134063600