React 函数式组件优化

前言

React 推出后,前后出现了三种定义组件的方式,分别是:css

  • 函数式组件
  • React.createClass 建立组件
  • React.Component 建立组件

相信你们在平常中使用的最多的仍是函数式组件和 React.Component 组件吧,今天就简单的说下函数式组件的两个优化方法。react

函数式组件

什么是函数式组件

在谈到函数式组件以前咱们先看一个概念 - 纯函数数组

何为纯函数?dom

引用一段维基百科的概念。函数

在程序设计中,若一个函数符合如下要求,则它可能被认为是纯函数性能

此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值之外的其余隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。测试

该函数不能有语义上可观察的函数反作用,诸如“触发事件”,使输出设备输出,或更改输出值之外物件的内容等。优化

能够看到,纯函数有着相同的输入一定产生相同的输出,没有反作用的特性。this

同理,函数式组件的输出也只依赖于 propscontext,与 state 无关。spa

函数式组件的特色

  • 没有生命周期
  • 无组件实例,没有 this(相信不少同窗被 this 烦过)
  • 没有内部状态(state)

函数式组件的优势

  • 不须要声明 class,没有 constructorextends等代码,代码简洁,占用内存小。
  • 不须要使用 this
  • 能够写成无反作用的纯函数。
  • 更佳的性能。函数式组件没有了生命周期,不须要对这部分进行管理,从而保证了更好地性能。

函数式组件的缺点

  • 没有生命周期方法。
  • 没有实例化。
  • 没有 shouldComponentUpdate,不能避免重复渲染。

React.memo

一个例子

这里先看一个例子,代码以下,也可点击这里进行查看。

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>React.memo demo-1</div>
      <button onClick={onClick}>update {n}</button>
      <Child />
    </div>
  );
}

const Child = () => {
  console.log("render child");
  return <div className="child">Child Component</div>;
};

const rootElement = document.getElementById("root");
render(<App />, rootElement);

复制代码

它实现了什么?就一个很简单的东西,一个父组件包了一个子组件。

这个你们判断一下,当我点击父组件的 button 更新 N 的时候,子组件中的 log 会不会执行?

按照通常的思惟来看,你父组件更新关我子组件什么事?我子组件的 DOM 又不用更新,既然没有更新,那还打什么 log

但实际效果是,每点击一次 button,子组件的 log 就会执行一次,虽然子组件的 DOM 没更新,但并不表明子组件的渲染函数没有执行。

如下是执行的效果图。

React.memo demo-1.gif

优化

针对上述状况,class 组件可使用 shouldComponentUpdate 来进行优化,可是函数式组件呢?React 一样也提供了一个优化方法,那就是 React.memo

memomemorized,意思是记住。若是输入的参数没有变,依据纯函数的定义,输出也不会变,那么直接返回以前记忆的结果不就好了。

const Child = React.memo(() => {
  console.log("render child");
  return <div className="child">Child Component</div>;
});
复制代码

Child 使用 React.memo 处理一下,这样的话,不管你点击多少次父组件的 button,子组件的 log 都不会执行。

完整代码点这里

效果图以下:

React.memo demo-2.gif

React.useCallback

咱们将上述代码稍微改一下, 让 Child 接受一个匿名函数,看看会产生什么后果。完整代码点这里

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>React.useCallback demo-1</div>
      <button onClick={onClick}>update {n}</button>
      <Child onClick={() => {}} />
    </div>
  );
}

const Child = React.memo((props: { onClick: () => void }) => {
  console.log("render child");
  return <div className="child">Child Component</div>;
});

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

观察代码能够看到也没啥变化嘛,只是子组件接受了一个空函数。

那么问题又来了,此次点击 button 子组件的 log 会执行吗?

看到这里各位同窗应该会想,每次都传一个空的匿名函数,props 也没变啊,那就不用从新渲染呗。具体结果如何,来看下效果:

React.useCallback demo-1.gif

能够看到每次点击 button 时,子组件的 log 依旧会再次执行。那么这是为何呢?

由于每次点击 button 更新父组件的时候,会从新生成一个空的匿名函数,虽然它们都是空的匿名函数,可是它们不是同一个函数。

函数是一个复杂类型的值,JavaScript 在比较复杂类型的值时,是对比它们的内存地址。不是同一个函数,那么内存地址也就不一样, React 会认为子组件的 props 发生了变化,子组件将从新渲染。

优化

那么怎么保证子组件每次都接受同一个函数呢?

很简单。既然父组件在更新的时候会从新生成一个函数,那么我把函数放到父组件外面不就能够了嘛,这样父组件在更新的时候子组件就会接受同一个函数。

代码以下。也可点击这里查看。

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

const childOnClick = () => {};

function App() {
  const [n, setN] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>React.useCallback demo-2</div>
      <button onClick={onClick}>update {n}</button>
      <Child onClick={childOnClick} />
    </div>
  );
}

const Child = React.memo((props: { onClick: () => void }) => {
  console.log("render child");
  return <div className="child">Child Component</div>;
});

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

效果图以下:

React.useCallback demo-2.gif

这样子看起来好像解决了子组件每次都接受不一样的函数致使从新渲染的问题,可是好像哪里不对劲,实现也不优雅。

缺点

若是子组件的函数依赖父组件里面的值,那么这种方式就不可行。

怎么办呢?若是能将函数也 memorized 就行了。

Hook

React16.8.0 的版本中正式推出了 Hooks,其中有一个 Hook 叫作 useCallback,它能将函数也 memorized 化。

useCallback 接受两个参数,第一个参数是一个函数,第二个参数是一个依赖数组,返回一个 memorized 后的函数。只有当依赖数组中的值发生了变化,它才会返回一个新函数。

看看使用 useCallback 后的代码,也能够点击这里查看。

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const childOnClick = React.useCallback(() => {
    console.log(`m: ${m}`);
  }, [m]);
  return (
    <div className="App">
      <div>React.useCallback demo-3</div>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        update n: {n}
      </button>
      <button
        onClick={() => {
          setM(m + 1);
        }}
      >
        update m: {m}
      </button>
      <Child onClick={childOnClick} />
    </div>
  );
}

const Child = React.memo((props: { onClick: () => void }) => {
  console.log("render child");
  return (
    <div className="child">
      <div>Child Component</div>
      <button onClick={props.onClick}>log m</button>
    </div>
  );
});

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

在上述代码中,子组件接受一个使用了 useCallback 的函数,它的依赖参数是 m,只有当 m 发生了变化,子组件接受的函数才会是一个从新生成的函数。也就是说,不管点击多少次更新 nbutton,子组件都不会更新,只有点击更新 mbutton 时,子组件才会更新。

看看效果如何:

React.useCallback demo-3.gif

实际效果符合咱们的预期。

歪心思

看到这里有些同窗就会想了,若是使用 useCallback 的时候,传一个空数组做为依赖数组,那么子组件就再也不受父组件的影响了,即便你父组件的 m 变化了,我子组件依旧不会从新渲染,这样子岂不是性能更好?话很少说,咱们来测试一下就行了。代码点击这里查看,效果以下:

React.useCallback demo-4.gif

能够看到,虽然子组件确实没重复渲染了,但一样的也致使一个问题,打印出来的 m 永远都是 0,再也读取不到更新后的 m 的值。

由此能够得出结论,传一个空数组做为依赖数组的后果就是,子组件接受的函数里面的参数永远都是初始化使用 useCallback 时的值,这样的结果并非咱们想要的。

因此歪心思仍是少来了。

总结

随着 React 正式推出 Hooks,带来一系列新的特性,极大地加强了函数式组件的功能,利用这些新特性能够实现和 class 组件同样的效果。

有了 React Hooks,咱们能够抛弃沉重的 class 组件,使用更加轻便,性能更加优异的函数式组件,所以掌握一些函数式组件的优化方法对咱们使用函数式组件开发是很是有用处的。

React Hooks 无论你香不香,反正我是先香了。

默默说一句,Vue 3.0 也会推出函数式组件。

相关文章
相关标签/搜索