React Hooks:优化性能

626 阅读4分钟

如果你已经使用 React 一段时间了,那么你应该了解到,在生产环境中开发人员会尽可能地优化其组件。在不必要的情况下,组件不应该被重新渲染。

 

函数组件中是怎么重新渲染的

几种方法是:

  • 更改组件状态

  • 更改组件道具

让我们看一个示例,由状态发生更改而重新渲染。

import React, {useState} from 'react';

function Demo() {
  const [count, setCount] = useState(0);
  const formatCounter = (e) => `value is ${e}`;

  return (
    <>
      {formatCounter(count)}
      <button onClick={() => setCount(prev => ++prev)}>+</button>
    </>
  );
}

当用户单击按钮时,我们调用 setCount() 方法,该方法将用新的 count 来重新渲染组件。在函数组件中,重新渲染意味着整个函数将再次运行。

  1. 因此,从顶部开始,useState() 方法将运行。由于钩子的固有特性,这将返回更新的 count 和缓存的setCount() 方法。

  2. 接下来,formatCounter() 函数将被添加到内存中。

  3. 最后返回jsx,但在 <button/> 上,有一个 onClick() 处理程序,它也会被添加到内存中。

每次组件重新渲染时,以上这些步骤都会发生,你需要以某种方式缓存 formatCounter()onClick() 方法,这样它们就不会在每次重新渲染时都被添加到内存中。

天真的解决方案

因此,首先想到的是将 formatCounter()onClick() 的处理程序移出组件。这样这些函数只会创建一次。

import React, {useState} from 'react';

const formatCounter = (e) => `value is ${e}`;
const onClick = (setCount) => setCount(prev => ++prev);

function Demo() {
  const [count, setCount] = useState(0);
  
  return (
    <>
      {formatCounter(count)}
      <button onClick={onClick}>+</button>
    </>
  );
}

formatCounter() 函数很容易提取,它不依赖于任何特定于组件的变量或方法。

但是 onClick() 方法不是这样,它依赖 setCount() 方法!

没有简单的方法来提取这个方法。所以它需要存在于组件内部,并且需要缓存,这样就不会在每次重新渲染时都将其添加到内存中。

useCallback()

useState() 类似,React 提供了一个名为 useCallback(Function, any[]) 的钩子,简言之,数组中变量或函数发生变化(浅层检查),你将得到一个新函数,否则你将得到一个缓存函数。

import React, {useState, useCallback} from 'react';

const formatCounter = (e) => `value is ${e}`;

function Demo() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => setCount(prev => ++prev), []);

  return (
    <>
      {formatCounter(count)}
      <button onClick={onClick}>+</button>
    </>
  );
}

当这个组件重新渲染时,onClick()formatCounter() 都不会再次存进内存,缓缓举起大拇指。

看到这你会发现然并卵,接着看。

假设一个组件在树下有 20 个组件。如果这 20 个组件中所有未优化的函数和事件处理程序都将再次添加到内存中。这取决于特定的情况,当用户在屏幕上输入或单击时,可能会使交互变得卡顿。简言之,未优化的函数会持续累加。

依赖数组

React 提供了另一个类似于 useCallback() 的钩子,称为 useMemo() 。它不接受回调函数,而是接受一个返回特定值的普通函数,这个函数需要始终有一个返回值。如果你发现该函数没有返回值,那么你可能需要改用 useCallback()

经过一些实践,这两个钩子之间的区别变得很明显。

还有其它的优化吗?

function Demo() {
  const name = ['a', 'b', 'c', 'd'];

  return (
      <div>{name}</div>
  );
}

在上面的示例中,name是一个数组,它在组件每次重新渲染时初始化。在这个示例中,天真的解决方案将把这个初始化从组件中抽离出去。但是,如果初始化依赖于组件本身,则应该使用 useMemo() 钩子并以这种方式对其进行优化。

现在我们还是使用 useMemo()

import React, {useMemo} from 'react';

function Demo() {
  const name = useMemo(()=> ['a', 'b', 'c', 'd'],[]);

  return (
      <div>{name}</div>
  );
}

现在让我们思考一个新的例子,其中初始化确实依赖于组件本身。

import React, {useState, useCallback, useMemo} from 'react';

function Demo(text) {
 const [state, setState] = useState(true);
 const onClick = useCallback(()=> setState(prev => !prev), []);  const obj = {
      a: state ? 1 : 2,
      b: !!state,
 };

 return (
      <>
        <div>{text}{obj.a}</div>
        <button onClick={onClick}>Change</button>
      </>
 );
}

这个例子有趣的是变量obj完全依赖于 state 。如果状态改变了,则此对象也将改变。

细心一点你会发现,组件接受一个 text 的属性参数,该参数仅用于返回的 jsx中。

现在想象一下,text 发生了变化,会发生什么事?

好吧,组件会重新渲染对吗?name会怎么样?

是的,你猜对了,它将被重新初始化并添加到内存中,但有必要这样做吗?我们需要重新初始化吗?当然不是,让我们使用 useMemo() 钩子来优化它。

import React, {useState, useCallback, useMemo} from 'react';

function Demo(text) {
 const [state, setState] = useState(true);
 const onClick = useCallback(()=> setState(prev => !prev), []); const obj = useMemo(()=>({
      a: state ? 1 : 2,
      b: !!state,
  }, []);

  return (
      <>
        <div>{text}{obj.a}</div>
        <button onClick={onClick}>Change</button>
      </>
  );
}

如此,obj变量现在已优化并准备就绪。哦,等等...

如果状态发生变化(通过单击按钮),您认为优化的obj会拿到正确的值吗?

当然不会,为什么呢?

如果提供给这些优化钩子的内容中有一些变量/函数/对象/数组,需要是最新的值,那么需要将其作为依赖项提供。

我需要 state 的最新值,以使name是正确的。如果不将 state 变量作为依赖项提供,name将始终是:

{
 a: 1,
 b: true,
}

这是刚开始 state 变量初始化为 true 之后初始化的变量。在那之后,它永远不会改变。

在本例中,我们希望它在 state 更改时更新。因此,我们只提供 state 作为依赖项,一切都很好。

import React, {useState, useCallback, useMemo} from 'react';

function Demo(text) {
 const [state, setState] = useState(true);
 const onClick = useCallback(()=> setState(prev => !prev), []); const obj = useMemo(()=>({
      a: state ? 1 : 2,
      b: !!state,
 }, [state]);

 return (
      <>
        <div>{text}{obj.a}</div>
        <button onClick={onClick}>Change</button>
      </>
 );
}

所以现在如果不调用setState() 函数,那么obj将始终返回缓存的值,并且不会每次都将其添加到内存中,比如 text 参数更改时。

总结

自己总结。