web前端 - React.memo

197 阅读3分钟

React.memo

什么是memo

React.memo是一个官方内置的高阶组件,使用memo包裹你的组件,当父组件重新渲染时,如果props没有发生改变,那子组件将不会重新渲染

React一次更新都会重新构建一颗组件树,经常会有很多不必要的开销,React.memo是一种性能优化的手段,让你可以跳过开销较大的组件的render。

官方文档:react.docschina.org/reference/r…

memo如何对比props

默认情况下,会使用Object.is比较每一个prop;memo的第二个参数接受一个对比函数,可以手动比较决定是否跳过更新。

memo(Component, function (oldProps, newProps) {
  return oldProps.id === newProps.id;
})

何时使用memo

官方推荐React.memo只在必要的时候使用,滥用会导致代码复杂度上升且增加阅读成本。

必要的场景同时满足:
1.相同的props总是渲染出想同的UI结果。
2.组件重新渲染出现严重延迟,不优化无法正常运行。

具体用例

首先我们写一个耗时组件, 经测试这个组件的render耗时可能长达1s以上(根据运行环境有差异)

export default function SlowComponent(props) {
  console.log('SlowComponent render start');
  const start = Number(new Date());
  let i = 0;
  while (i < 1000000000) {
    i += 1;
  }
  const end = Number(new Date());
  console.log('SlowComponent render end', end - start);
  return <div>{props.value}</div>
}

我们在页面中引入这个组件,以下是个很简单的页面,由一个计数元素、一个按钮和一个耗时组件组成。

function App() {
  const [count, setCount] = useState(0);
  const [value , setValue] = useState('');

  return (
    <div className="App">
      <div className="App-body">
        <div>count: { count }</div>
        <div onClick={() => setCount(count => count + 1)}>click</div>
        <SlowComponent value={value}/>
      </div>
    </div>
  );
}

// 点击按钮,打印SlowComponent render start

当我们点击按钮时,组件App的内部状态改变并重新渲染,这会导致所有自组件都触发重新渲染,包括耗时组件(尽管每个prop值都是相同的)。

此时,未优化耗时组件将渲染1s以上,新组件树的构建耗时被严重拖慢,导致视觉上的更新卡顿。

我们使用memo包裹耗时组件进行优化

export default React.memo(SlowComponent);

// 点击按钮,不打印SlowComponent render start

此时点击按钮,控制台将不在打印“SlowComponent render start”,耗时组件的渲染被跳过了,整个组件树的构建时间减去了一秒或更多,更新变的很流畅。

prop为对象、数组或者函数

上文提到默认的比较函数是使用Object.is对每个prop进行比较(浅对比),那么就会出现一个新的问题:当对象、数组、函数的引用改变时,“记忆化”就失效了。

以函数为例,我们修改上一小节的代码:让用户点击SlowComponent对应的元素后计数+1:

//App.js
function App() {
  const [count, setCount] = useState(0);
  const [value , setValue] = useState('点击 + 1');
  const [state] = useReducer(reducer, initState);

  const handleClick = () => setCount(count => count + 1);

  return (
    <div className="App">
      <header className="App-header">
        <div id="title">{state.title}</div>
      </header>
      <div className="App-body">
        <div>count: { count }</div>
        <SlowComponent value={value} handleClick={handleClick}/>
      </div>
    </div>
  );
}

//SlowComponent.js
function SlowComponent(props) {
  console.log('SlowComponent render start');
  const start = Number(new Date());
  let i = 0;
  while (i < 1000000000) {
    i += 1;
  }
  const end = Number(new Date());
  console.log('SlowComponent render end', end - start);
  return <div onClick={props.handleClick}>{props.value}</div>
}

export default React.memo(SlowComponent)

修改后的ui如下图:

image.png
点击“点击 + 1”后触发更新,此时控制台打印了SlowComponent render start,页面卡顿后更新

image.png

这是因为App每次render都创建了一个新的handleClick函数,引用和之前不一样了。要解决这个问题,需要让我们的handleClick也实现“记忆化”,这就需要用到useCallback或者useMemo。

useMemo & useCallback

我们尝试使用useCallback来优化上文的代码:

function App() {
  const [count, setCount] = useState(0);
  const [value , setValue] = useState('点击 + 1');
  const [state] = useReducer(reducer, initState);

  // 修改这一行
  const handleClick = useCallback(() => setCount(count => count + 1), []);

  return (
    <div className="App">
      <header className="App-header">
        <div id="title">{state.title}</div>
      </header>
      <div className="App-body">
        <div>count: { count }</div>
        <SlowComponent value={value} handleClick={handleClick}/>
      </div>
    </div>
  );
}

我们再次点击,发现页面不再卡顿更新,控制台未打印SlowComponent render start,SlowComponent没有re-render。

使用useMemo也可以实现类似的效果:

const handleClick = useMemo(() => () => setCount(count => count + 1), []);

对于prop是对象、数组、函数这种会改变引用的情况,需要借助useMemo和useCallback,让对应的prop具有“记忆化”功能,否则我们的性能优化很容易就失效