乐趣区

关于react.js:React-函数式组件怎样进行优化

前言

目标

本文只介绍函数式组件特有的性能优化形式,类组件和函数式组件都有的不介绍,比方 key 的应用。另外本文不具体的介绍 API 的应用,前面兴许会写,其实想用好 hooks 还是蛮难的。

面向读者

有过 React 函数式组件的实际,并且对 hooks 有过实际,对 useState、useCallback、useMemo API 至多看过文档,如果你有过对类组件的性能优化经验,那么这篇文章会让你有种相熟的感觉。

React 性能优化思路

我感觉 React 性能优化的理念的次要方向就是这两个:

  1. 缩小从新 render 的次数。因为在 React 里最重 (花工夫最长) 的一块就是 reconciliation(简略的能够了解为 diff),如果不 render,就不会 reconciliation。
  2. 缩小计算的量。次要是缩小反复计算,对于函数式组件来说,每次 render 都会从新从头开始执行函数调用。

在应用类组件的时候,应用的 React 优化 API 次要是:shouldComponentUpdatePureComponent,这两个 API 所提供的解决思路都是为了 缩小从新 render 的次数,次要是缩小父组件更新而子组件也更新的状况,尽管也能够在 state 更新的时候阻止以后组件渲染,如果要这么做的话,证实你这个属性不适宜作为 state,而应该作为动态属性或者放在 class 里面作为一个简略的变量。

然而在函数式组件外面没有申明周期也没有类,那如何来做性能优化呢?

React 实战视频解说:进入学习

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

当首次渲染的时候的成果如下:

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

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

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);

首次渲染的成果

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

而后当我点击 改副标题 这个 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>
  );
}

首次渲染的成果如下:

这个例子性能很简略,就是点击 +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 外面的计算属性有殊途同归的作用。

不过另外揭示两点

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

二、计算量如果很小的计算函数,也能够抉择不应用 useMemo,因为这点优化并不会作为性能瓶颈的要点,反而可能应用谬误还会引起一些性能问题。

总结

对于性能瓶颈可能对于小我的项目遇到的比拟少,毕竟计算量小、业务逻辑也不简单,然而对于大我的项目,很可能是会遇到性能瓶颈的,然而对于性能优化有很多方面:网络、要害门路渲染、打包、图片、缓存等等方面,具体应该去优化哪方面还得本人去排查,本文只介绍了性能优化中的冰山一角:运行过程中 React 的优化。

  1. React 的优化方向:缩小 render 的次数;缩小反复计算。
  2. 如何去找到 React 中导致性能问题的办法,见 useCallback 局部。
  3. 正当的拆分组件其实也是能够做性能优化的,你这么想,如果你整个页面只有一个大的组件,那么当 props 或者 state 变更之后,须要 reconciliation 的是整个组件,其实你只是变了一个文字,如果你进行了正当的组件拆分,你就能够管制更小粒度的更新。

正当拆分组件还有很多其余益处,比方好保护,而且这是学习组件化思维的第一步,正当的拆分组件又是一门艺术了,如果拆分得不合理,就有可能导致状态凌乱,多敲代码多思考。

退出移动版