【内容梳理】<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;参考上面【合成事件】那里的代码
相关推荐
zqx_714 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
TonyH20021 天前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
掘金泥石流1 天前
React v19 的 React Complier 是如何优化 React 组件的,看 AI 是如何回答的
javascript·人工智能·react.js
lucifer3111 天前
深入解析 React 组件封装 —— 从业务需求到性能优化
前端·react.js
秃头女孩y2 天前
React基础-快速梳理
前端·react.js·前端框架
sophie旭2 天前
我要拿捏 react 系列二: React 架构设计
javascript·react.js·前端框架
BHDDGT2 天前
react-问卷星项目(5)
前端·javascript·react.js
liangshanbo12152 天前
将 Intersection Observer 与自定义 React Hook 结合使用
前端·react.js·前端框架
黄毛火烧雪下2 天前
React返回上一个页面,会重新挂载吗
前端·javascript·react.js