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

129 阅读3分钟

1. 特殊说明

  • 没有被子组件使用的props会被丢弃,比如传递了style,但是子组件没有操作style属性,或者没有restProps,则style无效
    • React 就像函数一样,不使用就没有效果
    • Vue 会自动将多余属性挂载在 dom 上
  • 若父组件发生了re-render,默认情况下(不进行特殊处理),则子组件也会re-render
    • 默认不会对对 props 进行一致性判定来减少 re-render,就像内部函数一样
    • 可以结合 React.memo 来减少 re-render

以下内容都是聚焦于组件 props,或者是传递 props 给组件之前

2. 普通属性

  • 基本类型: number string boolean null undefined symbol bigint
  • 非基本类型,要么要求父组件使用useMemo包裹(不友好);要么在组件外层套React.memo,然后第二个参数进行比较,偷懒的方案是直接用react-fast-compare
import React from 'react';
import isEqual from 'react-fast-compare';
...
const UserSelector = () => {
  ...
}
export default React.memo(UserSelector, isEqual)
  • react 默认采用 浅比较(Object.is) 来判定数据是否发生变化
    • 非基本类型使用比较内存地址来判定是否变更,所以【直接修改对象的属性】或者【直接调用数组方法】不会\color{red}{不会} 修改内存地址,不会触发变更。
    • 对象可以监听内部基本类型属性,或者序列化
    • 数组重新定义数组方法,或者序列化

3. 回调函数属性

  • 使用匿名箭头函数

    • 可以传递额外参数
    • 无法解决:\color{red}{无法解决:}父组件 re-render,子组件也 re-render 的性能问题
    <TimePicker
      onChange={(value, valueStr) => handleChange(valueStr, 'minValue')}
      value={searchValue.minValue ? moment(searchValue.minValue, 'HH:mm:ss') : null}
    />
    
  • 不使用匿名箭头函数

    • 子组件触发时传递参数
    • 拆成多个事件
    • 使用 useMemoizedFn + React.memo来做到,在父组件发生 re-render 时,子组件的参数没有发生改变,子组件不 re-render
    // 父组件中
    const handleEdit = useMemoizedFn((id: string) => {
      ... // 其内可以使用本组件的外部值
    });
    
    <FormAction
      ...
      onEdit={handleEdit}
      ...
    />
    
    // 子组件中
    const FormAction = (props: IFormActionProps) => {
      const { ..., onEdit, ...  } = props;
      ...
    
      const handleClick = () => {
        onEdit?.(id)
      }
    }
    
    export default React.memo(FormAction, deepEqual);
    

4. 解构对象

  • 不使用解构的坏处\color{red}{坏处}
    • 每一次从大对象 props 中获取属性需要消耗性能
    • 每一次的值异常都要判定,比如【问号判空】、【感叹号断言】、【默认值赋予】等
    • 代码会不简洁,且会导致编译后的代码中出现很多es5的判定内容
const control = getControl(controlId, controls);
const originControl = getControl(control?.componentSettings?.srcColumnId || '', props.relatedForm?.template?.controls) || {};
return basicInputComponents(originControl.controlId, originControl.type, props.relatedForm?.template?.controls);

对比

const { relatedForm } = props;
const { template } = relatedForm || {};
// 别名
const { controls: relatedControls = [] } = template || {};
const control = getControl(controlId, controls);
const { componentSettings } = control || {};
const { srcColumnId = '' } = componentSettings || {};

const originControl = getControl(srcColumnId, relatedControls) || {};
return basicInputComponents(originControl.controlId, originControl.type, relatedControls);
  • 解构赋予默认值是在属性值为undefined的时候才生效,null不会生效
const targetObj = { a: 0, b: undefined, c: null, d: [], e: {} };
const { a = '默认值', b = '默认值', c = '默认值', d = '默认值', e = '默认值' } = targetObj;

console.log('a:', a); // 0
console.log('b:', b); // 默认值
console.log('c:', c); // null
console.log('d:', d); // []
console.log('e:', e); // {}
  • 若解构时赋予了默认【非基本类型】值,则在结合useEffect的过程中,有可能产生死循环
const { dataList = [] } = props;

const [dataObj, setDataObj] = useState({count: 0});

useEffect(() => {
  ...
  setCount({
     ...dataObj,
     count: dataList.length
  });
  ...
}, [dataList]);

5. React.memo包裹组件

  • 类似 class component 中的shouldComponentUpdate的逻辑
  • 组件被渲染时,若返回 false\color{red}{false},则会进行此次 re-render;返回 true\color{red}{true},则会忽略此次 re-render。可以有效避免父组件渲染导致的无效子组件渲染
  • 默认使用 Object.is浅比较的方式,来判定 props 前后是否发生了变化
  • props 中有函数时,需要结合useCallback(最好用useMemorizedFn替代)来保证回调函数的不变性
    • useCallback 无法有效处理有外部依赖的函数
  • 使用第二个参数来设置自定义逻辑(可以采用react-fase-compare插件)。逻辑同第二点
React.memo(someComponent, (prev, next) => {
  if (prev.id === next.id) {
    return true; // 忽略此次渲染
  }
  return false; // 进行此次渲染
});
  • 没有特殊判定,直接可以使用 react-fast-compare
import isEqual from 'react-fast-compare';
...
export default React.memo(MyComponent, isEqual);