手把手教你React(五)- 组件及优化

1,575 阅读7分钟

概述

组件是React的核心概念之一,在React的世界中可以说一切皆为组件,组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

函数组件和Class组件

组件的最简单的定义方式就是一个Javascript函数,像下面这样:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。

你同时还可以使用ES6的Class来定义组件:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

当我们写好组件之后,需要将组件渲染到对应的DOM节点:

function Welcome(props) {  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;ReactDOM.render(
  element,
  document.getElementById('root')
);

这样 我们一个最简单的组件已经渲染到了root节点。需要注意的是Props是只读的,props是用来在组件之间进行通信的数据,不论在函数组件还是Class组件中都不可修改组件的props,React的严格的规则是所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。可以在组件中修改的内容是state,在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

State和生命周期

state我们在上一张状态管理中进行了较为详细的阐述,state的原则是:

  1. 不要直接修改state,构造函数是唯一可以通过this.state来初始化state的地方,在其它地方则应该通过setState方法来修改state。
  2. state的更新可能是异步的,于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。因为 this.props this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。
  3. state的更新会被合并,当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

React函数式组件的生命周期在组件的首次渲染阶段会调用:

  1. componenWillMount
  2. render
  3. componentDidMount

在更新阶段会调用:

  1. componentWillReceiveProps
  2. componentShouldUpdate
  3. componentWillUpdate
  4. render
  5. componentDidUpdate

由于React的concurrent模式为了性能的优化将组件更新之前的阶段拆分成了可间断的过程,所以在concurrent模式下componenWillMount、componentWillReceiveProps、componentWillUpdate不再推荐使用,取而代之的是getDerivedStateFromProps和getSnapshotBeforeUpdate。

我们通过生命周期实现一个计时器的组件:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {    
    this.setState({ 
     date: new Date()   
    });  
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

hooks

在React16.8的版本之前,函数式的组件并不能在其中使用生命周期,这使得我们在设计组件的时候通常将只负责渲染的木偶组件定义为函数式组件,而将处理复杂逻辑和用户交互的组件用Class的方式描述,而在React16.8的版本中诞生的hooks这项新的特性则允许我们在不编写Class组件的情况下仍能使用state 和其它特性。

hooks诞生的背景:

  1. 复杂组件变得难以理解:我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
  2. 组件之间复用状态逻辑变得很难:React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。通过 Hooks 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

 常用的React内置hooks主要包括允许我们使用和修改state的useState和允许我们进行副作用操作的useEffect。

import React, { useState, useEffect } from 'react';
function Example() {
  const [count, setCount] = useState(0);

  //
  useEffect(() => {    document.title = `You clicked ${count} times`;  });
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

其等价的Class组件类似于:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {    document.title = `You clicked ${this.state.count} times`;  }  
  componentDidUpdate() {    document.title = `You clicked ${this.state.count} times`;  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

可以看出useState类似与setState,约定了state的内容和对应的修改方式,而useEffect则像是Class组件中的生命周期,而且可以看出相比与生命周期,只要产生副作用的地方都会调用useEffect,而不需要我们在每个生命周期中去手动添加。

组件性能优化

前面的文章有提到过在React中数据是自顶向下单向流动的,也就是我们常说的单向数据流,当我们更新一个组件的时候,该组件的子孙组件都会重新渲染,尽管大部分组件并不需要被更新。所以React在组件更新之前提供了shouldComponentUpdate生命周期,让开发者可以自行判断该组件是否需要更新,从而避免组件不必要的更新,达到性能上的优化。举个例子:

shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

同时,React还提供了PureComponent这样的组件,PureComponent组件会自动的对组件的props和state进行浅比较,而不需要手动在shouldComponentUpdate生命周期中进行处理。

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

需要注意的是PureComponent只会进行浅比较,当state某种程度可变的话,会造成一定的遗漏。

而在函数式组件中,React新增了React.memo API实现了类似于shouldComponentUpdate和PureComponent的机制。

const MyComponent = React.memo(function MyComponent(props) {
    /* 使用 props 渲染 */
});

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseContext 的 Hook,当 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

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

总结:

  1. React组件主要包括函数式组件和Class组件
  2. 组件中主要包括props、state和生命周期。
  3. 在老版本的React中我们只能在Class函数中使用state和副作用代码,hooks允许我们在函数组件中使用副作用。
  4. 相较于render props和高阶组件,hooks提供了更好抽离组件复用逻辑的方式。
  5. React组件开发中可以使用shouldComponentUpdate,PureComponent,和React.memo来优化组件避免不必要的渲染。