如何对 React 函数式组件进行优化

文章首发我的博客javascript

前言

目的

本文只介绍函数式组件特有的性能优化方式,类组件和函数式组件都有的不介绍,好比 key 的使用。另外本文不详细的介绍 API 的使用,后面也许会写,其实想用好 hooks 仍是蛮难的。php

面向读者

有过 React 函数式组件的实践,而且对 hooks 有过实践,对 useState、useCallback、useMemo API 至少看过文档,若是你有过对类组件的性能优化经历,那么这篇文章会让你有种熟悉的感受。html

React 性能优化思路

我以为React 性能优化的理念的主要方向就是这两个:前端

  1. 减小从新 render 的次数。由于在 React 里最重(花时间最长)的一块就是 reconction(简单的能够理解为 diff),若是不 render,就不会 reconction。vue

  2. 减小计算的量。主要是减小重复计算,对于函数式组件来讲,每次 render 都会从新从头开始执行函数调用。java

在使用类组件的时候,使用的 React 优化 API 主要是:shouldComponentUpdatePureComponent,这两个 API 所提供的解决思路都是为了减小从新 render 的次数,主要是减小父组件更新而子组件也更新的状况,虽然也能够在 state 更新的时候阻止当前组件渲染,若是要这么作的话,证实你这个属性不适合做为 state,而应该做为静态属性或者放在 class 外面做为一个简单的变量 。react

可是在函数式组件里面没有声明周期也没有类,那如何来作性能优化呢?api

React.memo

首先要介绍的就是 React.memo,这个 API 能够说是对标类组件里面的 PureComponent,这是能够减小从新 render 的次数的。数组

可能产生性能问题的例子

举个🌰,首先咱们看两段代码:缓存

在根目录有一个 index.js,代码以下,实现的东西大概就是:上面一个 title,中间一个 button(点击 button 修改 title),下面一个木偶组件,传递一个 name 进去。

// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from './child'

function App() {
  const [title, setTitle] = useState("这是一个 title")

  return (
    <div className="App"> <h1>{ title }</h1> <button onClick={() => setTitle("title 已经改变")}>更名字</button> <Child name="桃桃"></Child> </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement); 复制代码

在同级目录有一个 child.js

// child.js
import React from "react";

function Child(props) {
  console.log(props.name)
  return <h1>{props.name}</h1>
}

export default Child
复制代码

当首次渲染的时候的效果以下:

image-20191030221223045

而且控制台会打印"桃桃”,证实 Child 组件渲染了。

接下来点击更名字这个 button,页面会变成:

image-20191030222021717

title 已经改变了,并且控制台也打印出"桃桃",能够看到虽然咱们改的是父组件的状态,父组件从新渲染了,而且子组件也从新渲染了。你可能会想,传递给 Child 组件的 props 没有变,要是 Child 组件不从新渲染就行了,为何会这么想呢?

咱们假设 Child 组件是一个很是大的组件,渲染一次会消耗不少的性能,那么咱们就应该尽可能减小这个组件的渲染,不然就容易产生性能问题,因此子组件若是在 props 没有变化的状况下,就算父组件从新渲染了,子组件也不该该渲染。

那么咱们怎么才能作到在 props 没有变化的时候,子组件不渲染呢?

答案就是用 React.memo 在给定相同 props 的状况下渲染相同的结果,而且经过记忆组件渲染结果的方式来提升组件的性能表现。

React.memo 的基础用法

把声明的组件经过React.memo包一层就行了,React.memo实际上是一个高阶函数,传递一个组件进去,返回一个能够记忆的组件。

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

那么上面例子的 Child 组件就能够改为这样:

import React from "react";

function Child(props) {
  console.log(props.name)
  return <h1>{props.name}</h1>
}

export default React.memo(Child)
复制代码

经过 React.memo 包裹的组件在 props 不变的状况下,这个被包裹的组件是不会从新渲染的,也就是说上面那个例子,在我点击更名字以后,仅仅是 title 会变,可是 Child 组件不会从新渲染(表现出来的效果就是 Child 里面的 log 不会在控制台打印出来),会直接复用最近一次渲染的结果。

这个效果基本跟类组件里面的 PureComponent效果极其相似,只是前者用于函数组件,后者用于类组件。

React.memo 高级用法

默认状况下其只会对 props 的复杂对象作浅层对比(浅层对比就是只会对比先后两次 props 对象引用是否相同,不会对比对象里面的内容是否相同),若是你想要控制对比过程,那么请将自定义的比较函数经过第二个参数传入来实现。

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

此部分来自于 React 官网

若是你有在类组件里面使用过 shouldComponentUpdate() 这个方法,你会对 React.memo 的第二个参数很是的熟悉,不过值得注意的是,若是 props 相等,areEqual 会返回 true;若是 props 不相等,则返回 false。这与 shouldComponentUpdate 方法的返回值相反。

useCallback

如今根据上面的例子,再改一下需求,在上面的需求上增长一个副标题,而且有一个修改副标题的 button,而后把修改标题的 button 放到 Child 组件里。

把修改标题的 button 放到 Child 组件的目的是,将修改 title 的事件经过 props 传递给 Child 组件,而后观察这个事件可能会引发性能问题。

首先看代码:

父组件 index.js

// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from "./child";

function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");

  const callback = () => {
    setTitle("标题改变了");
  };
  return (
    <div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button> <Child onClick={callback} name="桃桃" /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 复制代码

子组件 child.js

import React from "react";

function Child(props) {
  console.log(props);
  return (
    <> <button onClick={props.onClick}>改标题</button> <h1>{props.name}</h1> </> ); } export default React.memo(Child); 复制代码

首次渲染的效果

image-20191031235605228

这段代码在首次渲染的时候会显示上图的样子,而且控制台会打印出桃桃

而后当我点击改副标题这个 button 以后,副标题会变为「副标题改变了」,而且控制台会再次打印出桃桃,这就证实了子组件又从新渲染了,可是子组件没有任何变化,那么此次 Child 组件的从新渲染就是多余的,那么如何避免掉这个多余的渲染呢?

找缘由

咱们在解决问题的以前,首先要知道这个问题是什么缘由致使的?

我们来分析,一个组件从新从新渲染,通常三种状况:

  1. 要么是组件本身的状态改变

  2. 要么是父组件从新渲染,致使子组件从新渲染,可是父组件的 props 没有改版

  3. 要么是父组件从新渲染,致使子组件从新渲染,可是父组件传递的 props 改变

接下来用排除法查出是什么缘由致使的:

第一种很明显就排除了,当点击改副标题 的时候并无去改变 Child 组件的状态;

第二种状况好好想一下,是否是就是在介绍 React.memo 的时候状况,父组件从新渲染了,父组件传递给子组件的 props 没有改变,可是子组件从新渲染了,咱们这个时候用 React.memo 来解决了这个问题,因此这种状况也排除。

那么就是第三种状况了,当父组件从新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name,一个是 onClickname 是传递的常量,不会变,变的就是 onClick 了,为何传递给 onClick 的 callback 函数会发生改变呢?在文章的开头就已经说过了,在函数式组件里每次从新渲染,函数组件都会重头开始从新执行,那么这两次建立的 callback 函数确定发生了改变,因此致使了子组件从新渲染。

如何解决

找到问题的缘由了,那么解决办法就是在函数没有改变的时候,从新渲染的时候保持两个函数的引用一致,这个时候就要用到 useCallback 这个 API 了。

useCallback 使用方法

const callback = () => {
  doSomething(a, b);
}

const memoizedCallback = useCallback(callback, [a, b])
复制代码

把函数以及依赖项做为参数传入 useCallback,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。

那么能够将 index.js 修改成这样:

// index.js
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Child from "./child";

function App() {
  const [title, setTitle] = useState("这是一个 title");
  const [subtitle, setSubtitle] = useState("我是一个副标题");

  const callback = () => {
    setTitle("标题改变了");
  };

  // 经过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
  const memoizedCallback = useCallback(callback, [])
  
  return (
    <div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button> <Child onClick={memoizedCallback} name="桃桃" /> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 复制代码

这样咱们就能够看到只会在首次渲染的时候打印出桃桃,当点击改副标题和改标题的时候是不会打印桃桃的。

若是咱们的 callback 传递了参数,当参数变化的时候须要让它从新添加一个缓存,能够将参数放在 useCallback 第二个参数的数组中,做为依赖的形式,使用方式跟 useEffect 相似。

useMemo

在文章的开头就已经介绍了,React 的性能优化方向主要是两个:一个是减小从新 render 的次数(或者说减小没必要要的渲染),另外一个是减小计算的量。

前面介绍的 React.memouseCallback 都是为了减小从新 render 的次数。对于如何减小计算的量,就是 useMemo 来作的,接下来咱们看例子。

function App() {
  const [num, setNum] = useState(0);

  // 一个很是耗时的一个计算函数
  // result 最后返回的值是 49995000
  function expensiveFn() {
    let result = 0;
    
    for (let i = 0; i < 10000; i++) {
      result += i;
    }
    
    console.log(result) // 49995000
    return result;
  }

  const base = expensiveFn();

  return (
    <div className="App"> <h1>count:{num}</h1> <button onClick={() => setNum(num + base)}>+1</button> </div>
  );
}
复制代码

首次渲染的效果以下:

useMemo

这个例子功能很简单,就是点击 +1 按钮,而后会将如今的值(num) 与 计算函数 (expensiveFn) 调用后的值相加,而后将和设置给 num 并显示出来,在控制台会输出 49995000

可能产生性能问题

就算是一个看起来很简单的组件,也有可能产生性能问题,经过这个最简单的例子来看看还有什么值得优化的地方。

首先咱们把 expensiveFn 函数当作一个计算量很大的函数(好比你能够把 i 换成 10000000),而后当咱们每次点击 +1 按钮的时候,都会从新渲染组件,并且都会调用 expensiveFn 函数并输出 49995000。因为每次调用 expensiveFn 所返回的值都同样,因此咱们能够想办法将计算出来的值缓存起来,每次调用函数直接返回缓存的值,这样就能够作一些性能优化。

useMemo 作计算结果缓存

针对上面产生的问题,就能够用 useMemo 来缓存 expensiveFn 函数执行后的值。

首先介绍一下 useMemo 的基本的使用方法,详细的使用方法可见官网

function computeExpensiveValue() {
  // 计算量很大的代码
  return xxx
}

const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
复制代码

useMemo 的第一个参数就是一个函数,这个函数返回的值会被缓存起来,同时这个值会做为 useMemo 的返回值,第二个参数是一个数组依赖,若是数组里面的值有变化,那么就会从新去执行第一个参数里面的函数,并将函数返回的值缓存起来并做为 useMemo 的返回值 。

了解了 useMemo 的使用方法,而后就能够对上面的例子进行优化,优化代码以下:

function App() {
  const [num, setNum] = useState(0);

  function expensiveFn() {
    let result = 0;
    for (let i = 0; i < 10000; i++) {
      result += i;
    }
    console.log(result)
    return result;
  }

  const base = useMemo(expensiveFn, []);

  return (
    <div className="App"> <h1>count:{num}</h1> <button onClick={() => setNum(num + base)}>+1</button> </div>
  );
}
复制代码

执行上面的代码,而后如今能够观察不管咱们点击 +1多少次,只会输出一次 49995000,这就表明 expensiveFn 只执行了一次,达到了咱们想要的效果。

小结

useMemo 的使用场景主要是用来缓存计算量比较大的函数结果,能够避免没必要要的重复计算,有过 vue 的使用经历同窗可能会以为跟 Vue 里面的计算属性有殊途同归的做用。

不过另外提醒两点

1、若是没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值;

2、计算量若是很小的计算函数,也能够选择不使用 useMemo,由于这点优化并不会做为性能瓶颈的要点,反而可能使用错误还会引发一些性能问题。

总结

对于性能瓶颈可能对于小项目遇到的比较少,毕竟计算量小、业务逻辑也不复杂,可是对于大项目,极可能是会遇到性能瓶颈的,可是对于性能优化有不少方面:网络、关键路径渲染、打包、图片、缓存等等方面,具体应该去优化哪方面还得本身去排查,本文只介绍了性能优化中的冰山一角:运行过程当中 React 的优化。

  1. React 的优化方向:减小 render 的次数;减小重复计算。
  2. 如何去找到 React 中致使性能问题的方法,见 useCallback 部分。
  3. 合理的拆分组件其实也是能够作性能优化的,你这么想,若是你整个页面只有一个大的组件,那么当 props 或者 state 变动以后,须要 reconction 的是整个组件,其实你只是变了一个文字,若是你进行了合理的组件拆分,你就能够控制更小粒度的更新。

合理拆分组件还有不少其余好处,好比好维护,并且这是学习组件化思想的第一步,合理的拆分组件又是一门艺术了,若是拆分得不合理,就有可能致使状态混乱,多敲代码多思考。

推荐文章

我这里只介绍了函数式组件的优化方式,更多的 React 优化技巧能够阅读下面的文章:

后记

我是桃翁,一个爱思考的前端er,想了解关于更多的前端相关的,请关注个人公号:「前端桃园」,若是想加入交流群关注公众号后回复「微信」拉你进群

相关文章
相关标签/搜索