再学 React Hooks (二):函数式组件性能优化

这是我参与更文挑战的第 10 天,活动详情查看:更文挑战

前言

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

Hook函数式组件让我们更加方便地开发 React 应用。本文将介绍函数式组件性能优化的几个方法。

减少 render 次数

使用 React.memo

React v16.6.0 提供了 React.memo() 这个 API 来解决前后 props 相同组件却仍然 render 的问题。

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

使用示例如下(示例来自官方文档):

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
});
复制代码

React.memo 的注意点:

  1. React.memo 仅检查 props 变更。如果函数式组件内部有 useState、useReducer 和 useReducer 的 Hook,当 context 发生变化时,它仍会重新渲染。
  2. React.memo 只对 props 复杂对象做浅比较。如果要自定义比较,需要传入比较函数作为第二个参数。

使用示例如下(示例来自官方文档):

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
}
export default React.memo(MyComponent, areEqual);
复制代码

使用 useCallBack、useMemo 缓存传给子组件的 props

现有下面的代码:

// 父组件
export default function App() {
  const [appCount, setAppCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const handleAppAdd = () => {
    setAppCount(appCount + 1);
  };

  const handleChildAdd = () => {
    setChildCount(childCount + 1);
  };

  console.log('app render');

  return (
    <div>
      <h2>App</h2>
      <button onClick={handleAppAdd}>appCount + 1</button>
      <p>appCount: {appCount}</p>

      <h2>child</h2>
      <button onClick={handleChildAdd}>childCount + 1</button>
      <Child value={childCount} />
    </div>
  );
}

// child 组件
import React from 'react';
const Child = props => {
  console.log('child render');
  const { value } = props;
  return <div>Child: {value}</div>;
};
export default React.memo(Child);

复制代码

上面的 Child 组件已经使用 React.memo 优化,当仅有父组件 appCount 变化时, Child 组件不会重新渲染。

现在我们在改造 Child 组件:

import React from 'react';
const Child = props => {
  console.log('child render');
  const { value, handleAdd } = props;
  return <div>
	  <p>Child: {value}</p>
    <button onClick={handleAdd}>btn in child: childCount + 1</button>
  </div>;
};
export default React.memo(Child);
复制代码

上面的代码中我们在子组件也加一个可以增减 ChildCount 的按钮,点击它时调用父组件中传入的 props.handleAdd

// app.jsx
export default function App() {
  const [appCount, setAppCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const handleAppAdd = () => {
    setAppCount(appCount + 1);
  };

  const handleChildAdd = () => {
    setChildCount(childCount + 1);
  };

  console.log('app render');

  return (
    <div>
      <h2>App</h2>
      <button onClick={handleAppAdd}>appCount + 1</button>
      <p>appCount: {appCount}</p>

      <h2>child</h2>
      <button onClick={handleChildAdd}>childCount + 1</button>
      <Child value={childCount} handleAdd={handleChildAdd} />
    </div>
  );
}
复制代码

上面的代码给 Child 传入了 handleAdd 的 props。然而当我们再次点击 appCount + 1 时,虽然 childCount 未变化,但是 Child 却刷新了,日志如下:

image-20210616201446852

从上面的情况我们可以分析出,是新传入 Childprops.handleAdd导致了重新渲染。因为 handleAddApp 组件中每次都是重新生成的一个新函数,React.memo 浅比较 props 改变,Child 重新 render。

那么我们怎么解决 props 上存在函数的问题呢?React.useCallback 可以解决这个问题。

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

由官方文档可以看出,只要 deps 未改变, useCallback返回的还是同一个函数。同样地 ,如果 props 是复杂对象, 我们也可以使用 useMemo 来处理。

上面 child 重复 render 的问题, 我们如下处理:

// App.js

const handleChildAdd = useCallback(() => {
  setChildCount(childCount + 1);
}, [childCount]);

复制代码

点击这里查看示例代码

占位组件的 render 优化

我们通常有一这样的需求,比如子组件有一个占位区域,占位区域的组件需要通过 props 传入,我们可以通过在父组件中创建好 React.Element 来减少占位组件的渲染次数。

image-20210617132917614

代码如下:

const Content = () => {
  console.log('Content render');
  return <div>content</div>;
};

// 优化前
export default function App() {
  return <Child content={Content} />;
}

// 优化后
export default function App() {
  return <Child content={<Content />} />;
}


复制代码

上面的代码中, 我们要注意 Child 组件传入 content 的方式。

  1. 优化前:传入的是 Content 组件
  2. 优化后:传入的是 <Content /> 形式的 React.Element

通过在父组件件内部使用 <Content /> 创建 React.Element,这样可以避免子组件自身 render 重新创建 ContentReact.Element

React.Context 读写分离

读写分析部分的参考文章

未分离前:

const LogContext = React.createContext();

function LogProvider({ children }) {
  const [logs, setLogs] = useState([]);
  const addLog = (log) => setLogs((prevLogs) => [...prevLogs, log]);
  return (
    <LogContext.Provider value={{ logs, addLog }}>
      {children}
    </LogContext.Provider>
  );
}

作者:ssh_晨曦时梦见兮
链接:https://juejin.cn/post/6889247428797530126
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

优化前的代码中,只要 setLogs 更新了状态, value 就会传入新的值。所有用到 logContext 对应的函数都会被更新。

优化后的代码:

function LogProvider({ children }) {
  const [logs, setLogs] = useState([]);
  const addLog = useCallback((log) => {
    setLogs((prevLogs) => [...prevLogs, log]);
  }, []);
  return (
    <LogDispatcherContext.Provider value={addLog}>
      <LogStateContext.Provider value={logs}>
        {children}
      </LogStateContext.Provider>
    </LogDispatcherContext.Provider>
  );
}

作者:ssh_晨曦时梦见兮
链接:https://juejin.cn/post/6889247428797530126
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复制代码

上面我们通过 LogDispatcherContextLogStateContext 实现读写分离。只需要使用 addLog 的组件就不会因为 logs 改变而刷新了。

参考资料

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享