【内容梳理】<React性能优化>降低re-render之state

1. 说明

以下针对的是useState、useReducer等

  • 直接修改 state 的值无法刷新页面
  • useState 的参数只在初始化时有效,若需根据外部的变更调整 state,请结合 useEffect 来更新 state
  • 调用 set 函数 后,当前事件中 state 还是旧值。(即使在同步事件中)
  • setState 没有渲染后的回调函数(和 class 写法不一致),只有传递函数的模式
ts 复制代码
setState((prevState) => (prevState += 1));

2. state 类型

  • 尽量使用 基本类型 属性,比较时方便
  • 若必须使用复杂对象,或多个基本属性有联动,则建议使用reducer来统一管理
ts 复制代码
export const RnRecordReduder = (state: IRnRecordState, action: IRnRecordAction): IRnRecordState => {
  switch (action.type) {
    case 'SET_FORM_VALUE': {
      ...
    }
    case 'SET_RELATION_VALUE': {
      ...
    }
    default: {
      return state;
    }
  }
};
...
const [store, dispatch] = useReducer(RnRecordReduder, {
  initialData,
  formValue: cloneDeep(initialData),
  onChangeFormValue,
});
...
dispatch({
  type: 'SET_RELATION_VALUE',
  value: (preState: Map<string, Record<string, any>>) =>
    update(preState || new Map<string, Record<string, any>>(), { $add: [[element.id, selRelation]] }),
});
  • 修改复杂大对象时,建议使用immerimmutability-helper等库进行对象操作,可以保证除了修改点和父级点之外的点没有变动;进而减少子组件的 re-render

3. 使用中的 tips

  • 调用 set 函数时,传递函数
    • 函数参数可以拿到上一次的 state 值;
    • 可以防止高频次操作,state 状态不对的情况(比如勾选和取消勾选的列表显示)
tsx 复制代码
const [curSelValue, setSelValue] = useState<IUserSelectAttr[]>([]); // 当前选中的用户
...

const handleChange = (event: CheckboxChangeEvent, selItem: IUserSelectAttr) => {
  ...
  setSelValue((prevSelValue) => {
    return prevSelValue.concat(selItem);
  });
  ...
};
  • React 接管事件中(合成事件等),setState 是异步更新的且会合并,即多次 setState,只会有一次 re-render,且 re-render 时拿到的 state 是上次的 state;
  • 非 React 接管事件中(包含生命周期),比如 PromisesetTimeoutaddEventListener 等,setState 是同步更新且不会合并,多次 setState 会产生多次 re-render( <math xmlns="http://www.w3.org/1998/Math/MathML"> R e a c t 18 \color{red}React18 </math>React18 后都会合并更新,并只 re-render 一次; <math xmlns="http://www.w3.org/1998/Math/MathML"> R e a c t 18 \color{red}React18 </math>React18 之前,可以尝试使用unstable_batchupdate
ts 复制代码
import { useUpdateEffect, useWhyDidYouUpdate } from 'ahooks';
import { Button } from 'antd';
import { useEffect, useState } from 'react';
import uniqid from 'uniqid';

const allFavorite = ['篮球', '看书', '打游戏', '跑步', '逛街', '锻炼', '做手工', '麻将', '扑克牌', '追剧'];

const SyncAndAsyncUpdate = () => {
  const [uniqKey1, setUniqKey1] = useState<string>();
  const [uniqKey2, setUniqKey2] = useState<string>();

  const [count, setCount] = useState(0); // number类型测试
  const [name, setName] = useState(''); // string类型测试
  const [isLoading, setLoading] = useState(false); // boolean类型测试
  const [favorite, setFavorite] = useState<Array<string>>([]); // 数组测试

  useWhyDidYouUpdate('SyncAndAsync', { count, name, isLoading, favorite });

  const randomSet = () => {
    const radomNum = Math.floor(Math.random() * 10);
    console.log('当前的radomNum:', radomNum);
    setCount(radomNum);
    console.log('当前的count:', count);
    setName(`Lawson-${radomNum}`);
    setLoading((prev) => !prev);
    setFavorite((prev) => prev.concat(allFavorite[radomNum]));
  };

  useEffect(() => {
    const targetDom = document.getElementById('addEventListener-test');
    const clickListener = () => {
      console.log('---addEventListener测试---');
      randomSet();
    };
    targetDom?.addEventListener('click', clickListener);
    return () => {
      targetDom?.removeEventListener('click', clickListener);
    };
  }, []);

  useUpdateEffect(() => {
    console.log('---同步effect测试---');
    randomSet();
  }, [uniqKey2]);

  useUpdateEffect(() => {
    console.log('---异步effect测试---');
    (async () => {
      await Promise.resolve();
      randomSet();
    })();
  }, [uniqKey1]);

  const handleProxy = () => {
    console.log('---react托管事件测试---');
    randomSet();
  };

  const handleSync = () => {
    setUniqKey2(uniqid());
  };

  const handleAsync = () => {
    setUniqKey1(uniqid());
  };

  const handleSetTimeout = () => {
    console.log('---setTimeout测试---');
    setTimeout(() => {
      randomSet();
    }, 200);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', width: '400px' }}>
      <Button onClick={handleProxy}>react托管事件测试</Button>
      <Button id="addEventListener-test">addEventListener测试</Button>
      <Button onClick={handleSync}>同步effect测试</Button>
      <Button onClick={handleAsync}>异步effect测试</Button>
      <Button onClick={handleSetTimeout}>setTimeout测试</Button>
    </div>
  );
};

export default SyncAndAsyncUpdate;
  • 高频次修改 state 时,看情况使用【防抖】或【节流】

4. 其他

  • 计算 state,使useMemo来计算出数据,以保证计算数据的不变性的方式,降低 re-render(注意依赖)
    • useMemo
    • useCreation
  • 不希望某个 state,或被解构的属性作为依赖项,但是希望在 useMemo 或 useEffect 中使用时,可以考虑使用useLatest; 以降低可能发生变动的源头的方式,来降低 re-render 的可能性
tsx 复制代码
// 当前成员的联动值
const chainUser: IUserSelectAttr = useCreation(() => {
  ...
}, [formValue, defaultValue, defaultMode]);

const chainUserLst = useLatest(chainUser);

// 联动数据发生修改,则同步更新当前成员值
useEffect(() => {
  ...
  onChange?.(chainUserLst.current);
  ...
}, [chainUser?.userCode, defaultMode]);
  • 为了解决异步数据 undefined 的问题,一般需要设置 isLoading 状态,来区分两种渲染 div; 此时需要注意 setLoading 要放在最后,因为很有可能当前处于同步更新中,不会 batchUpdate;参考上面【合成事件】那里的代码
相关推荐
Rsun045513 小时前
React相关面试题
前端·react.js·前端框架
我命由我123457 小时前
React - state、state 的简写方式、props、props 的简写方式、类式组件中的构造器与 props、函数式组件使用 props
前端·javascript·react.js·前端框架·html·html5·js
C澒7 小时前
React + TypeScript 编码规范|统一标准 & 高效维护
前端·react.js·typescript·团队开发·代码规范
@大迁世界10 小时前
精通 React 面试:从零到中高级
前端·javascript·react.js·面试·前端框架
无知的小菜鸡11 小时前
React 零散知识记录
前端·react.js·前端框架
我命由我1234512 小时前
React - React 初识、创建虚拟 DOM 的两种方式、jsx 语法规则、React 定义组件
前端·javascript·react.js·前端框架·html·html5·js
白兰地空瓶14 小时前
手写 Mini React:从 0 实现 createElement 和 render,理解 React 的底层原理
react.js
我命由我1234516 小时前
前端开发 - this 指向问题(直接调用函数、对象方法、类方法)
开发语言·前端·javascript·vue.js·react.js·html5·js
Olafur_zbj16 小时前
【AI】深度解析OpenClaw智能体循环(Agentic Loop):底层运行机制、ReAct演进与多智能体协同架构
人工智能·react.js·架构·agent·openclaw
我命由我1234516 小时前
React - ref、回调 ref 回调执行次数的问题、createRef 函数、事件处理
前端·javascript·react.js·前端框架·html·html5·js