【内容梳理】<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;参考上面【合成事件】那里的代码
相关推荐
知识分享小能手4 小时前
React学习教程,从入门到精通,React AJAX 语法知识点与案例详解(18)
前端·javascript·vue.js·学习·react.js·ajax·vue3
NeverSettle_9 小时前
React工程实践面试题深度分析2025
javascript·react.js
学前端搞口饭吃9 小时前
react reducx的使用
前端·react.js·前端框架
努力往上爬de蜗牛9 小时前
react3面试题
javascript·react.js·面试
开心不就得了10 小时前
React 进阶
前端·javascript·react.js
谢尔登10 小时前
【React】React 哲学
前端·react.js·前端框架
学前端搞口饭吃12 小时前
react context如何使用
前端·javascript·react.js
GDAL12 小时前
为什么Cesium不使用vue或者react,而是 保留 Knockout
前端·vue.js·react.js
Dragon Wu1 天前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
YU大宗师1 天前
React面试题
前端·javascript·react.js