乐趣区

关于react.js:怎样对reacthooks进行性能优化

前言

当初越来越多人开始应用 React Hooks + 函数组件的形式构筑页面。函数组件简洁且优雅,通过 Hooks 能够让函数组件领有外部的状态和副作用(生命周期),补救了函数组件的有余。

但同时函数组件的应用也带来了一些额定的问题:因为函数式组件外部的状态更新时,会从新执行一遍函数,那么就有可能造成以下两点性能问题:

  1. 造成子组件的非必要从新渲染
  2. 造成组件外部某些代码(计算)的反复执行

好在 React 团队也意识到函数组件可能产生的性能问题,并提供了 React.memouseMemouseCallback 这些 API 帮忙开发者去优化他们的 React 代码。在应用它们进行优化之前,我想咱们须要明确咱们应用它们的目标:

  1. 缩小组件的 非必要从新渲染
  2. 缩小组件 外部的反复计算

1 应用 React.memo 防止组件的反复渲染

在讲述 React.memo 的作用之前,咱们先来思考一个问题:什么状况下须要从新渲染组件?

一般来讲以下三种状况须要从新渲染组件:

  1. 组件外部 state 发生变化时
  2. 组件外部应用的 context 发生变化时
  3. 组件内部传递的 props 发生变化时

当初咱们先只关注第 3 点:props 发生变化时从新渲染,这种状况是一种现实状况。因为如果一个父组件从新渲染,即便其子组件的 props 没有产生任何变动,这个子组件也会从新渲染,咱们称这种渲染为 非必要的从新渲染。这时 React.memo 就能够派上用场了。

首先 React.memo 是一个 高阶组件

高阶组件(Higher Order Component)相似一个工厂:将一个组件丢进去,而后返回一个被加工过的组件。

React.memo 包裹的组件在渲染前,会对新旧 props 进行 浅比拟

  • 如果新旧 props 浅比拟相等,则不进行从新渲染(应用缓存的组件)。
  • 如果新旧 props 浅比拟不相等,则进行从新渲染(从新渲染的组件)。

上述的解释可能会比拟形象,咱们来看一个具体的例子:

import React, {useState} from 'react';

const Child = () => {console.log('Child 渲染了');
  return <div>Child</div>;
};

const MemoChild = React.memo(() => {console.log('MemoChild 渲染了');
  return <div>MemoChild</div>;
});

function App() {const [isUpdate, setIsUpdate] = useState(true);
  const onClick = () => {setIsUpdate(!isUpdate);
    console.log('点击了按钮');
  };
  return (
    <div className="App">
      <Child />
      <MemoChild />
      <button onClick={onClick}> 刷新 App </button>
    </div>
  );
}

export default App;
复制代码

上例中:Child 是一个一般的组件,MemoChild 是一个被 React.memo 包裹的组件。

当我点击 button 按钮时,调用 setIsUpdate 触发 App 组件从新渲染(re-render)。

控制台后果如下:

如上图:
首次渲染时,ChildMemoChild 都会被渲染,控制台打印 Child 渲染了memoChild 渲染了。

而当我点击按钮触发从新渲染后,Child 依旧会从新渲染,而 MemoChild 则会进行新旧 props 的判断,因为 memoChild 没有 props,即新旧 props 相等(都为空),则 memoChild 应用之前的渲染后果(缓存),防止了从新渲染。

由此可见,在没有任何优化的状况下,React 中某一组件从新渲染,会导致其 全副的子组件从新渲染。即通过 React.memo 的包裹,在其父组件从新渲染时,能够防止这个组件的非必要从新渲染。

须要留神的是:上文中的【渲染】指的是 React 执行函数组件并生成或更新虚构 DOM 树(Fiber 树)的过程。在渲染实在 DOM(Commit 阶段)前还有 DOM Diff 的过程,会比对虚构 DOM 之间的差别,再去渲染变动的 DOM。不然如果每次更改状态都会从新渲染实在 DOM,那么 React 的性能真就爆炸了(笑)。

更多 react 面试题解答参见 前端 react 面试题具体解答

2 应用 useMemo 防止反复计算

const memolized = useMemo(fn,deps)

React 的 useMemo 把【计算函数 fn】和【依赖项数组 deps】作为参数,useMemo 会执行 fn 并返回一个【缓存值 memolized】,它仅会在某个依赖项扭转时才从新计算 memolized。这种优化有助于防止在每次渲染时都进行高开销的计算。具体应用场景能够参考下例:

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

function App() {const [list] = useState([1, 2, 3, 4]);
  const [isUpdate, setIsUpdate] = useState(true);
  const onClick = () => {setIsUpdate(!isUpdate);
    console.log('点击了按钮');
  };

  // 一般计算 list 的和
  console.log('一般计算');
  const sum = list.reduce((previous, current) => previous + current);

  // 缓存计算 list 的和
  const memoSum = useMemo(() => {console.log('useMemo 计算');
    return list.reduce((previous, current) => previous + current);
  }, [list]);

  return (
    <div className="App">
      <div> sum:{sum}</div>
      <div> memoSum:{memoSum}</div>
      <button onClick={onClick}> 从新渲染 App</button>
    </div>
  );
}

export default App;
复制代码

上例中:sum 是一个依据 list 失去的一般计算值,memoSum 是一个通过 useMemo 失去的 momelized 值(缓存值),并且依赖项为 list

如上图控制台中 log 所示:

  1. 首次渲染,summemoSum 都会依据 list 的值进行计算;
  2. 当点击【从新渲染 App】按钮后,尽管 list 没有扭转,然而 sum 的值进行了从新计算,而 memoSum 的值则没有从新计算,应用了上一次的计算结果(memolized)。
  3. 当点击【往 List 增加一个数字】按钮后,list 的值产生扭转,summemoSum 的值都进行从新计算。

总结:在函数组件外部,一些 基于 State 的衍生值和一些简单的计算 能够通过 useMemo 进行性能优化。

3 应用 useCallback 防止子组件的反复渲染

const memolizedCallback = useCallback(fn, deps);

React 的 useCallback 把【回调函数 fn】和【依赖项数组 deps】作为参数,并返回一个【缓存的回调函数 memolizedCallback】(实质上是一个援用),它仅会在某个依赖项扭转时才从新生成 memolizedCallback。当你把 memolizedCallback 作为参数传递给子组件(被 React.memo 包裹过的)时,它能够防止非必要的子组件从新渲染。

useCallback 与 useMemo 异同

useCallbackuseMemo 都会缓存对应的值,并且只有在依赖变动的时候才会更新缓存,区别在于:

  • useMemo 会执行传入的回调函数,返回的是 函数执行的后果
  • useCallback 不会执行传入的回调函数,返回的是 函数的援用

useCallback 应用误区

有很多初学者(包含以前的我)会有这样一个误区:在函数组件外部申明的函数全副都用 useCallback 包裹一层,认为这样能够通过防止函数的反复生成优化性能,实则不然:

  1. 首先,在 JS 外部函数创立是十分快的,这点性能问题不是个问题(参考:React 官网文档:Hook 会因为在渲染时创立函数而变慢吗?)
  2. 其次,应用 useCallback 会造成额定的性能损耗,因为减少了额定的 deps 变动判断。
  3. 每个函数用 useCallback 包一层,不仅显得臃肿,而且还须要手写 deps 数组,额定减少心智累赘。

useCallback 正确的应用场景

  1. 函数组件外部定义的函数须要 作为其余 Hooks 的依赖
  2. 函数组件外部定义的函数须要传递给其子组件,并且 子组件由 React.memo 包裹

场景 1:useCallback 次要是为了防止当组件从新渲染时,函数援用变动所导致其它 Hooks 的从新执行,更为甚者可能造成组件的有限渲染:

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

function App() {const [count, setCount] = useState(1);
  const add = () => {setCount((count) => count + 1);
  };
  useEffect(() => {add();
  }, [add]);
  return <div className="App">count: {count}</div>;
}

export default App;
复制代码

上例中,useEffect 会执行 add 函数从而触发组件的从新渲染,函数的从新渲染会从新生成 add 的援用,从而触发 useEffect 的从新执行,而后再执行 add 函数触发组件的从新渲染 …,从而导致有限循环:

useEffect 执行 -> add 执行 -> setCount 执行 -> App 从新渲染 -> add 从新生成 -> useEffect 执行 -> add 执行 -> …

为了防止上述的状况,咱们给 add 函数套一层 useCallback 防止函数援用的变动,就能够解决有限循环的问题:

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

function App() {const [count, setCount] = useState(1);
  // 用 useCallback 包裹 add,只会在组件第一次渲染生成函数援用,之后组件从新渲染时,add 会复用第一次生成的援用。const add = useCallback(() => {setCount((count) => count + 1);
  }, []);
  useEffect(() => {add();
  }, [add]);
  return <div className="App">count: {count}</div>;
}

export default App;
复制代码

场景 2:useCallback 是为了防止因为回调函数援用变动,所导致的子组件非必要从新渲染。(这个子组件有两个前提:首先是接管回调函数作为 props,其次是被 React.memo 所包裹。)

const Child = React.memo(({onClick}) => {console.log(`Button render`);
  return (
    <div>
      <button onClick={onClick}>child button</button>
    </div>
  );
});

function App() {const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  // 状况 1:未包裹 useCallback
  const onClick = () => {setCountA(countA + 1);
  };
  // 状况 2:包裹 useCallback
  const onClick = useCallback(() => {setCountA(countA + 1);
  }, []);
  return (
    <div>
      <div>countA:{countA}</div>
      <div>countB:{countB}</div>
      <Child onClick={onClick1} />
      <button onClick={() => setCountB(countB + 1)}>App button</button>
    </div>
  );
}
复制代码

上例中,Child 子组件由 React.memo 包裹,接管 onClick 函数作为 props 参数。

  • 状况 1:onClick 未包裹 useCallback,当点击 app button 时,触发从新渲染,onClick 从新生成函数援用,导致 Child 子组件从新渲染。
  • 状况 2:onClick 包裹 useCallback,当点击 app button 时,触发从新渲染,onClick 不会生成新的援用,防止了 Child 子组件从新渲染。

4 总结

上文叙述中,咱们通过 React.memouseMemouseCallback 这些 API 防止了在应用函数组件的过程中可能触发的性能问题,总结为一下三点:

  • 通过 React.memo 包裹组件,能够防止组件的非必要从新渲染。
  • 通过 useMemo,能够防止组件更新时所引发的反复计算。
  • 通过 useCallback,能够防止因为函数援用变动所导致的组件反复渲染。
退出移动版