React性能优化(三):使用性能优化API


theme: orange

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情

大家好,我是疯狂的小波。在前面2节中,我们介绍了一文看懂React优化原理及方案、以及第一种性能优化方案将可变部分与不变部分分离

这一节,我们就来看看第二种优化方案:使用性能优化API

为什么要使用性能优化API?

还记得我们在之前的示例吗?

function SelectCar() {
  const [brand, setBrand] = useState({}); 
  const [count, setCount] = useState(0);

  return (
    <>
      {count}
      <button onClick={() => setCount(count + 1)}>修改count</button>
      <BrandList brand={brand} />
      <Car />
    </>
  );
}

上述代码中,当点击buttoncount+1SelectCar 组件重新渲染;子组件 BrandListCar 也会重新渲染一次,哪怕此时子组件数据没有任何变化。如果这2个组件内部,还有子组件,也会全部重新渲染。

这种可以使用 将可变部分与不变部分分离 的方案进行优化。但是有时我们无法将所有内容都进行分离。那就必需要使用其他的方案了。

先来思考下,为什么会出现这种问题?是 React 的 bug 吗?

为什么出现性能问题?

在上一节中,我们提到过,只有当组件的state数据propsstatecontext)变更时,组件才会重新渲染。而上面的 count 数据变更时,BrandListCar 组件的state数据并没有变更,为什么也会重新渲染?

这是因为在 React 内部,props 判断是否变更默认是使用的全等比较。而每次父组件重新渲染时,子组件的 props 都是生成的一个新对象,全等判断前后 props 不相等。所以导致父组件每次更新,子组件也会重新渲染。

可以在数据变更时,进行源代码调试,在 react-dom.development.jsbeginWork 函数中添加断点,当第一个参数 current 指向指定组件时。可以看下其中 oldPropsnewProps 的对比。内部是通过 oldProps !== newProps 进行全等判断。所以哪怕上面的 Car 组件没有任何 props 属性,props 也是一个空对象,对比前后 props 时,{} !== {},不相等,重新渲染。

那怎么解决这种情况呢?那就是将 props 对象的全等比较,修改为 props 中属性值的比较。 这样我们就能够控制这种非必要的更新了。

// 修改前
oldProps !== newProps

// 修改后
oldProps.key !== newProps.key

解决方案:使用 React.memo 包裹组件

const BrandList: React.FC = props => {
    // ...
}

export default React.memo(BrandList);

React.memo 包裹的组件,props 对比时会做浅比较,比较其中属性的值是否相等。如果前后 props 中属性值相等,就不会更新组件。这是避免组件重复渲染最常用的优化手段。

在上面的例子中,count再次变更时,BrandList组件也不会再重新渲染。因为这个时候对比 props 是对比 brand 属性值是否相等,很明显是没有变更的,所以最终判断我们的组件无变更,不重新渲染,BrandList 内部的子组件也不会重新渲染。如果我们的 props 有多个属性,则会依次进行对比,只要有任一不想等,则会重新渲染。

所以,其实我们每个组件都可以用 memo 包裹,来提高我们页面的更新性能(除非组件非常的小,重新渲染的损耗可以忽略不计)。或者针对项目中结构比较复杂、性能损耗比较严重的子树针对性进行性能优化。

一、使用 memo 包裹组件需要注意的问题

1.引用数据类型的props,修改属性值未修改引用地址 – 组件不更新

function SelectCar() {
  const [brand, setBrand] = useState({
      name: '宝马',
      detail: {
        carName: "530",
        code: 2
      },
      info: {}
  });
  
  const changeBrand = () => {
      const newBrand = {...brand}
      newBrand.detail.code = 3
      setBrand(newBrand)
  }

  return (
    <>
      <button onClick={changeBrand}>修改品牌</button>
      <BrandList brandDetail={brand.detail} />
    </>
  );
}

我们修改一下之前的例子,BrandList组件现在接收brand.detailprop

当我们点击修改品牌按钮,修改了detail内部属性code,并将整个brand对象重新赋值,SelectCar组件会重新渲染,但是发现 BrandList 组件并不会重新渲染,内部的code属性还是2。这是因为 brand.detail 的值并没有变,还是之前的引用地址,组件内部做浅比较的时候会判定前后 props 相等,不进行更新。

这个就是我们在使用 memo 的过程中,比较容易忽略的引用类型的更新问题。

而针对这个问题,常见的解决方案有:
  • 如果对象只有一个层级:可以简单的使用浅拷贝,如展开运算符或 Object.assign 将对象重新赋值

  • 如果对象层级是多层:可以使用 immutable 数据

immutable 简介

immutable 数据也被称为不可变数据,内部采用是多叉树的结构,凡是有节点被改变,那么它和与它相关的所有上级节点都更新。如果是没有关联的其他同级节点,并不会更新。

通俗点理解,immutable 对象的数据变更,会自动修改相关对象的引用地址,不相关的则不会修改。

像上例中,当brand对象使用immutable数据时,brand.detail.code变更,会自动修改brandbrand.detail的引用地址,brand.info则不会修改。

可能我们会觉得,直接深拷贝 brand 属性,这样就能确保所有子级都会更新了。虽然这样可以达到更新的目的,但是如果当brand属性有其他节点(如brand.info)并且传递给其他组件时,那也会导致这些组件被动更新。这样违背了我们使用memo最初的目的:只更新需要更新的组件

所以,采用 immutable 能够最大效率地更新数据结构,并且同时满足我们 memo 浅比较的需求,目前来说是比较好的方案。唯一的缺点就是使用上麻烦一点,immutable 数据需要和js数据进行转换。

immutable

2.容易忽略的props中的函数

还是和上面demo一样,BrandList组件内部使用memo,这次组件额外接收了一个函数类型的prop

function SelectCar() {
  const [brand, setBrand] = useState({}); 
  const [count, setCount] = useState(0);
    
  const onSelect = () => {
      // ...
  }
  
  return (
    <>
      {count}
      <button onClick={() => setCount(count + 1)}>修改count</button>
      <BrandList brand={brand} onSelect={onSelect} />
    </>
  );
}

此时,当我们的 count 属性变更时,发现 BrandList 组件还是会重新渲染。但是BrandListbrandonSelect属性好像都没有变啊。

这是因为 组件重新渲染时,组件内的函数也会被重新创建

上例中count 变更时,SelectCar 组件重新渲染,onSelect 函数又会重新进行赋值,虽然内容没有变,但是函数是新创建的,引用地址已经变了。所以导致 BrandList 判定前后 props 不一致,进行更新。

这里其实 onSelect 函数是完全没有变化的。我们期望这种场景下 BrandList 也不需要更新。

此时,可以使用 useCallBack 对我们的函数进行缓存。

const onSelect = useCallback(() => {
  // ...
}, [])

这样,当我们的 count 再次变更时,BrandList 组件也不会更新了。

如果我们的 onSelect 函数内部依赖页面数据,也可以将依赖项添加到依赖数组中。

const onSelect = useCallback(() => {
  // ...
}, [brand])

这样,只有当我们的 brand 属性变更时,函数才会重新生成。

但是在部分场景下,如果依赖项频繁变化,那优化的效果可能就不明显了。不过后续React会出一个新的hook:useEvent,目前处于 RFC 阶段。它既能够保证缓存的函数引用始终是同一个,又能保证函数内每次都能拿到最新的、正确的 state,可以完美解决这个问题。

二、延伸思考:是否所有的方法都需要添加 useCallback 进行缓存,以提升性能?

否。useCallback 的原理都是 利用闭包缓存上次结果,会有额外的内存与比较逻辑。
并不是绝对优化,而是一种成本交换,并非适用所有场景。所以应该根据实际场景来判断是否使用。

何时使用:

  • 会影响到子组件的非必要更新时(如上)
  • 当计算过程或函数体足够复杂时

何时不使用:

  • 组件只会渲染一次时
  • 依赖项频繁变动时(useEvent更合适)
对于useMemo的使用场景也是和useCallback类似

当计算过程足够复杂时,比如特别大数据的处理,复杂度较高的计算(多层嵌套循环等),可以使用进行缓存优化。

useMemo 常用的2个场景

  • 缓存计算结果
const sum = useMemo(() => {
    let _sum = 0;
    for (let i = 1; i <= target; i++) {
      _sum += i;
    }
    return _sum;
}, [target]);

这个示例中,当组件重新渲染时:target属性未发生变化,则直接使用上次计算的结果;否则重新执行计算函数。

  • 缓存DOM节点或组件
const child1 = useMemo(() => <Child1 a={a} />, [a]);

return (
    <>
      {child1}
    </>
)

比如页面中一个长列表的渲染,就可以使用 useMemo 缓存列表渲染结果。

当用于缓存组件时,和 React.memo 类似,不过更推荐使用 React.memo ,代码可读性会更好。

class组件中实现该性能优化

使用 PureComponent,会判断 class 组件的 stateprops 属性是否有变更,也是浅比较,如果都没有变更则不重新渲染。

与使用 shouldComponentUpdate 生命周期效果一样,只是更方便。

class App extends React.PureComponent {
    
}

总结

当父组件数据变更时,会导致所有子孙组件全部重新渲染,造成大量的性能损耗。

此时我们可以通过使用 React.memo 包裹组件,来达到性能优化,这样只有当 props 内部属性值变更时,组件才会重新渲染。

使用 React.memo 进行优化时,需要注意2个点:

1、引用数据类型的prop,直接修改内部值未修改引用地址时组件不会更新。此时可以根据情况进行数据的浅拷贝或者immutable数据来达到更新。

2、当子组件props中有函数时,可能会导致memo失效。这是因为父组件重新渲染时,组件内的函数也会被重新创建,导致函数的prop前后值不一致,子组件被动重新渲染。此时可以通过 useCallback 对函数进行缓存,避免子组件的非必要重渲染。

但是不要滥用useMemouseCallback,只有在需要的时候才使用,否则可能反而会影响性能。

class 组件中,可以使用 React.PureComponent 进行类似的优化,原理也是差不多的。

这2节,我们讲到了如何避免 React 组件非必要的重新渲染,来提高性能。下一节,我们就通过 提高页面渲染效率 看看,当组件数据更新时,怎么提高页面渲染的效率。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容