React.memo() VS useMemo()

3,233 阅读4分钟

使用React技术栈的同学,对 React.memo() 方法和 React-Hooks 已经不陌生了,其原理和作用,我就不在这里多说了,不是本文重点(如果对 Hooks 不太明白的,请自行到官网查阅文档)。今天要讲的是,如何让 Class 组件也可以舒服的用上 useMemo 这个 Hook 函数,升华 render 方法中的 UI 组件渲染性能优化。

小背景

相信大家在一些复杂业务场景开发中,难免会需要使用 Class 组件的进行开发。当 render 函数遇到 列表渲染复杂逻辑渲染 ,我们想降低计算量来提高性能时,渲染逻辑的组织就有点尴尬了。

class APP extends PureComponent {
    state = {
        data: []
    }
    add = () => {
        const {data} = this.state;
        data.push(data.length);
        this.setState({ data });
    }
    render() {
        return <div>
            <button onClick={this.add}>加一</button>
            {<!--如果上层组件传入的props频繁更新的话,这里的长列表渲染的性能就有点低了。-->}
            {this.state.data.map((_, idx) => {
              return <h5>这是第 { idx } 项!</h5>;
            })}
        </div>
    }
}

这时候你可能就好想直接把 useMemo() 这个 Hook 拎过来用啊。但是,这时 eslint-react-hooks-pluginReact-Runtime 就不答应了,问你: “为啥要在 Class 组件中使用 Hooks 呢?Hooks 乃函数组件专属方法,Class 调用者死!”。

好吧,那就规矩点儿,要用 memo 组件,要么用函数组件?Ok,我们先来看下解决方案:

可选方案

1. 使用React.memo()函数包裹一下渲染逻辑,然后控制 memo 组件的 props 依赖 ,最后在 render 中引入组件并传参。

class APP extends PureComponent {
    ...
    render() {
        return <div>
            <button onClick={this.add}>加一</button>
            <List data={this.state.data}/>
        </div>
    }
}
// 列表组件
const List = React.memo(({ data }: { data: number[] }) => {
  return (
    <>
        return data.map((_, idx) => {
          return <h5>这是第 { idx } 项!</h5>;
        });
    </>
  );
}, (prev, next) => prev.data.length !== next.data.length)

这就有点烦人了,每次数据变更依赖的处理都需要在 memo 包装组件的第二个参数函数中去单独管理,而且 memo 的返回节点必须唯一,然后就写了一个 fragment 。为啥就不能像 useMemo 一样简单的去解决问题呢?

2. 将渲染部分的逻辑抽离成一个单独的 函数UI组件函数,然后在里面使用 各种 hooks 一顿骚操作,这看起来非常棒啊。

class APP extends PureComponent {
    ...
    render() {
        return <div>
            <button onClick={this.add}>加一</button>
            <List data={this.state.data}/>
        </div>
    }
}
// 列表组件
function List({ data }: { data: number[] }) {
  return (
    <>
      {useMemo(() => {
        return data.map((_, idx) => {
          return <h5>这是第 { idx } 项!</h5>;
        });
      }, [data])}
    </>
  );
}

这种方式看上去呢,和第一种方法没有什么区别嘛,而且都需要返回单一节点。

小结

以上两种方案存在的问题:

  • 依赖更新管理需要到单独的组件中进行管理,如果上层传参发生变化,需要维护两个物理位置的代码;
  • 对于小规模列表UI渲染的性能提升,代码修改量较高。
  • 若组件中存在大量的类似的列表或逻辑处理渲染 的小规模节点,全部抽离后,UI渲染的上下文关系变得不太清晰。

衍生方案

前面也说了,我们想要在 Class 组件中用到 useMemo 的简洁缓存和依赖处理能力。Ok,我们现在就按照这个思路开始造个小轮子。其实轮子我们已经造好了,只是需要稍加调教。不多说,上代码:

class APP extends PureComponent {
    ...
    render() {
        return <div>
            <button onClick={this.add}>加一</button>
            {<!--现在就可以像使用useMemo一样开心了,再也不用担心依赖管理和代码维护了,一个地方全搞定-->}
            <UseMemo deps={[counter]}>
                {() => {
                    return this.state.data.map((_, idx) => {
                      return <h5>这是第{idx}项!</h5>;
                    });
                }}
            </UseMemo>
        </div>
    }
}
// 通用UseMemo调用,并定义了 Typescript 的参数类型,方便调用时进行传参类型提示
function UseMemo<T>({ deps , children , wrap = Fragment }: { deps?: DependencyList; children(): T; wrap?: ComponentType; }) {
    // 转换参数为大写开头
    const Wrap = wrap;
    // 这里由于React 的限制,必须要返回单一节点,因此做了一层包装。
    // 包装节点可自定义传入一个组件类型
    return <Wrap>{useMemo(children, deps)}</Wrap>;
}

这里我们将 方案2 做了一次简单的抽象,然后就把 useMemo 的能力暴露给了 Class 组件,是不是简单的一批?但是是真的好用啊。

写在后面

其实这也不是什么新鲜玩意儿,不过是一次功能上的抽象。稍微有点 经验的同学应该立马就能想到。但是对于一些初来匝道的同学,在写业务的同时又想最求点性能,又不想多写代码的,就可以参考一下咯,哈哈哈。