阅读 178

再学 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>;
};


// 示例1
export default function App() {
  const Content = () => {
    console.log('Content render');
    return <div>content</div>;
  };
  return <Child content={Content} />;
}
const Child = (props) => {
  const Content = props.content
  return (
    <>
      <header>头部</header>
      <main>
        <Content />
      </main>
    </>
  )
}

// 示例2
export default function App() {
  return <Child content={Content} />;
}
const Child = (props) => {
  const Content = props.content
  return (
    <>
      <header>头部</header>
      <main>
        <Content />
      </main>
    </>
  )
}

// 示例3
export default function App() {
  return <Child content={<Content />} />;
}

const Child = (props) => {
  const Content = props.content
  return (
    <>
      <header>头部</header>
      <main>
        {props.content}
      </main>
    </>
  )
}

复制代码

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

  1. 示例1: 传入的是每次 render 都重新生成的 Content 组件
  2. 示例2:传入的是 Content 组件
  3. 示例3:传入的是 <Content /> 形式的 React.Element

性能对比如下

  1. 示例1示例2传入的都是 Content 组件,它们在 Child 每次 render 时都需要重新渲染 Content组件
  2. 示例1在父组件 render 时会生成新的 Content 组件,这会导致每次 render 过程中 Content 组件 生成的 React.Element 对应的 type 都会不同, 从而 Content 组件 会不停的销毁创建。
  3. 示例3相对而言是最优方案。相对示例1,父组件 render 时不会销毁创建 Content。 相对示例2Child组件 renderContent组件不会重新 render

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 改变而刷新了。

惰性初始值

惰性初始值主要参考 React 官方文档的 hooks-faq 部分

惰性创建初始 state

正常使用 useState 的情况:

const [state, setState] = useState(initialState);
复制代码

initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用。

惰性初始 state 文档

惰性创建 useRef() 的初始值

useRef 不会像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的

function Image(props) {
  const ref = useRef(null);

  // ✅ IntersectionObserver 只会被惰性创建一次
  function getObserver() {
    if (ref.current === null) {
      ref.current = new IntersectionObserver(onIntersect);
    }
    return ref.current;
  }

  // 当你需要时,调用 getObserver()
  // ...
}
复制代码

参考资料

文章分类
前端
文章标签