阅读 320

React 组件与设计模式之一

image.png

组件化已然成为构建前端应用的指导思想了 —— 将整个庞大的应用拆分成各个小的功能模块,再将功能模块划分到页面,每个实际的页面再由独立的功能单元(组件)组装而成。在整个过程中,组件扮演着最基础而又最重要的角色。既然组件是构建应用的基础,为了比较全面的了解组件以及组件的设计模式,下面我们将从 React 组件的演进史开始本次的探索......

组件的创建

image.png

到目前为止,React 组件的创建形式经历了三个标志性的阶段:

  1. ES5 盛行时期下的 React.createClass
  2. ES6+ 盛行时期下的 class Component 和 Stateless Component
  3. React Hooks 时期,也就是现阶段的 Functional Component


相信有很多同学上手 React 就是从编写 class Component 开始的,且目前处于 class Component 向 React Hooks 转型的阶段,而对最早的 ES5 语法的组件编写形式知之甚少。为了比较全面的了解 React 组件的演进,我们就重头开始说起!👏

React.createClass

React 于 2013年5月开源,那时正是 ES5 盛行的时期,这也就造就了 React 组件最早基于 ES5 语法创建的形式 —— React.createClass。其创建组件的基本示例代码如下:

// 创建一个 Input 组件
const Input = React.createClass({
  // 设置 props 传参类型
  propTypes: {
    initialValue: React.PropTypes.string,
  },

  // 组件默认的props值
  defaultProps: {
    initialValue: '',
  },

  // 设置 initial state
  getInitialState: function() {
    return {
      text: this.props.initialValue || '',
    }
  },

  // 定义 input onChange 方法
  handleChange: function(e) {
    this.setState({
      text: e.target.value,
    })
  },

  render: function() {
    const { text } = this.state;

    return (
      <div>
        <p>{text}</p>
        <input type="text" value={text} onChange={this.handleChange} />
      </div>
    );
  }
});
复制代码

这个方法的命名很符合当时业界大牛们极力模拟类语法的潮流!😁


其实熟练掌握 class Component 后,再回过头来看这种 ES5 的形式,就觉得非常简单了:主要逻辑是调用 React.createClass 方法,并以对象的形式传入组件需要实现的生命周期方法、render 方法、propTypes props 类型、defaultProps props 默认值、getInitialState 初始 state 值和其他逻辑函数的实现。通过配置的形式完成了创建组件所需参数的传入,很符合 ES5 函数式处理逻辑的方式,这在当时也是官方主推形式!

技术日新月异!在随后的几年中,ES6 以其新颖、简洁的语法迅速风靡前端界,而后 React 也在 V0.14 版本之后引入了 class Component 和 Stateless Component 两种创建组件的方式,并且在官方得到了很大的推荐。顺势在后续的迭代版本中代替了 ES5 的 React.createClass!所以该方法早已被完全废弃,同学们了解即可!😏

这里顺便多提一嘴:为了高效的构建 React 应用,Facebook 提供了 prop-types 工具用于检查组件 props 传参类型。而后为了减小 React core 包的体积,在 v15.5 版本后将 prop-types 相关逻辑提取到单独的 npm 包了,但是用法没变,反而将选择权留给了开发者 —— 在 prop-types、TypeScript 或者其他类型检查工具之间做选择!

class Component

class Component 是 ES6 class 语法造就的产物。因为是它引入了标准的 class 语法,终结了多年以来业界大牛们重复造模拟类轮子的行为。而 class Component 和它的名字一样,是以类语法为基础实现的。利用 class 语法创建组件的示例代码如下:

// 创建一个 Input 组件
export default class Input extends React.Component {
  constructor(props) {
    super(props);

    // 初始化 state
    this.state = {
      text: props.initialValue || 'placeholder'
    };

    // 为函数手动绑定 this
    this.handleChange = this.handleChange.bind(this);
  }

  // 定义 input 的 onChange 方法
  handleChange(e) {
    this.setState({
      text: e.target.value,
    })
  },
  
  render() {
    const { text } = this.state;

    return (
      <div>
        <p>{text}</p>
        <input type="text" value={text} onChange={this.handleChange} />
      </div>
    );
  }
}

// 设置 props 传参类型
Input.propTypes = {
  initialValue: React.PropTypes.string
};

// 组件默认的props值
Input.defaultProps = {
  initialValue: ''
};
复制代码


class Component 主要思想就是利用 ES6 class 语法通过 extends 继承 React.Componet 或 React.PureComponent 的方式创建组件。在实际的工程中,我们主要实现 render 方法和按需实现组件生命周期方法。

⚠️注意:特别小心处理 JSX 中函数调用时的 this 指向问题,否则你的代码有可能随时爆炸💥!

Stateless Component

class Component 完完全全就是面向对象的思想了。对于很多没有接触过面向对象编程的同学来说 class 语法也是他们使用 React 的一大障碍。什么继承呀,构造函数呀都特别烦人,还要处理各种生命周期方法更是烦上加烦。而且并不是所有情况下组件都需要管理自己的状态(state),有的组件的职责则只是接受数据,然后显示数据(这种组件后续会详细讲解)。于是乎,React 在 V0.14 后推出了 Stateless Component(无状态组件,即没有自己持有的 state)。示例代码如下:

export default function Button(props) {
  const { color, text } = props;
  
  return (
    <button className={`btn btn-${color}`}>
      <em>{text}</em>
    </button>
  );
}

Button.propTypes = {
  color: PropTypes.string,
  text: PropTypes.string,
};

Button.defaultProps = {
  color: 'blue',
  text: 'Click',
};
复制代码


这种组件创建的方式非常简单,就是一个函数,数据完全是从父组件传递过来的,处理的逻辑也很简单:拿到数据,然后展示数据。没有自己的 state 需要管理,this 不指向组件本身,不用操心生命周期。但是有个缺点:就是没有很好的方式避免无用的 rerender。虽然这样,但它仍然成为了 React 开发者的一大法宝!

class Component 和 Stateless Component 在很长一段时间一直是构建 React 应用的组件开发模式,但是苦于没有很好的组件逻辑复用方案(虽然社区和官方一直在探索)。为了将函数式编程和组件逻辑复用发挥到极致,Facebook 官方推出了 Hooks 的方案 —— 以函数式的编程方式最大的复用可复用的组件逻辑。

Functional Component

React 18.6 新增了 Hooks 特性,使我们可以摒弃(而非完全摒弃) class Component 的编程方式,将函数式编程进行的更彻底,同时也解决了长久以来代码逻辑复用难的问题,将 React 带入另一个崭新的阶段!函数式组件示例代码如下:

export default function CountButton(props) {
  const { color, text } = props;
  const [count, setCount] = React.useState(0);
  const setCountMemo = React.useCallback(() => setCount(count => count + 1), []);
  
  return (
    <>
      <p>{count}</p>
      <button
        className={`btn btn-${color}`}
        onClick={setCountMemo}
      >
        <em>{text}</em>
      </button>
    </>
  );
}

CountButton.propTypes = {
  color: PropTypes.string,
  text: PropTypes.string,
};

CountButton.defaultProps = {
  color: 'blue',
  text: 'Click',
};
复制代码


额。。。是不是似曾相识的感觉?这是你可以翻回到上面的 Stateless Component 再看看(当然,如果你非常熟悉那就没必要了)。它也只是声明了一个函数,但是逻辑里面有一些 Stateless Component 所没有的东西 —— 它可以拥有并管理属于自己的 state,而且还加入了 memoize 方法对函数和组件进行了优化处理。

总结

Hooks 是 React 后续发展的趋势,但是它完全是可选的。官方已经声明:Hooks 完全是可选的,也就是你不需要将已有的代码全部利用 Hooks 重写,而且也不是必须现在就去学、去用;Hooks 是 100% 向后兼容的,而且官方还没有计划移除 class Component。所以在实际的项目开发中,根据自己团队的实际情况和业务需求,综合考量,选择适合自己项目的组件开发方式,而不是一味的盲目跟从!

组件设计原则

单一职责

单一职责原则用来约束一个类只负责一项职责,是面向对象编程的主要思想之一。该原则同样适用于 React 组件!我们希望你在规划组件时,可以将组件当作一种函数或者是对象来考虑,根据单一功能原则来判定组件的范围。也就是说,一个组件原则上只能负责一个功能。如果它需要负责更多的功能,这时候就应该考虑将它拆分成更小的组件。这样设计的好处有很多:

  • 可以降低组件的复杂度,一个组件只负责一个功能,其逻辑肯定要比负责多个功能的组件简单得多;
  • 由于组件只负责一个功能,所以它肯定是小而美的,从而提高了组件的可读性,增强了系统的可维护性;
  • 变更引起的风险降低了。虽然变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响;
  • 单个组件的复用性提高了。因为组件只负责一个功能,所以重复该功能的地方,可以很好的复用该组件逻辑。

#### 开闭原则 在设计模式中,该原则的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。这条原则对组件依然有效!组件在设计之初,我们应该分析需要抽离的逻辑,然后将这些逻辑进行抽象,将抽象的逻辑封装于底层的组件。在业务应用中根据实际的场景再对这个底层的组件进行引用包装,而不是直接修改它。如果后续功能又有延伸,依然是进行扩展。这样会保持底层根基不变,而功能会向多个方向进行扩张。相反,直接修改组件逻辑,可能会牵扯到很多看不见的组件逻辑变化,无形之中影响其他功能模块。

组合优于继承

这也是 React 官方所推崇的理念。

继承的起源,来自于多个类中相同特征和行为的抽象。子类可以通过继承父类,调用父类中定义的方法和属性,从而达到代码重用的目的。另外,子类除了重用父类的代码以外,还可以扩展自身的属性和方法,来描述子类特有的特征和行为。通过继承,可以达到代码很大程度上的复用,而且子类还可以很方便的扩展自己特有的属性和方法。

继承的优点:

  • 父类的大部分功能可以通过继承关系自动进入子类;
  • 修改或扩展继承而来的属性和方法较为容易。


但是也有缺点:

  • 无法通过继承达到多个类代码的重用
  • 父类的方法子类无条件继承,很容易造成方法污染
  • 从父类中继承的方法,是一种静态的复用。不能在运行时发生改变,不够灵活
  • 另外 JavaScript 中 class 并不直接支持多继承

继承可以用,但使用继承需要谨慎。一般来说,使用继承有两个条件:

  • 父类中所有的属性和方法,在子类中都适用。
  • 子类不需要再去重用别的类中的代码。


如果不能满足这两个条件,那么就应该使用聚合/组合关系去替代继承,来达到代码的复用。

而组合则是把一些特征和行为抽取出来,形成工具类,然后通过聚合/组合的方式成为当前类的属性,再调用其中的属性和行为达到代码重用的目的。通过聚合/组合关系,可以很好的解决继承的缺点。由于一个类可以建多个属性,也就是可以聚合多个类。所以可以通过聚合/组合关系,重用多个类中的代码。

组件分类

image.png

到目前为止,常用的创建 React 组件的方式有三种:class Component、Stateless Component 和 Functional Component,而它们各有其特色和使用场景。为了能够更好的区分它们的特点、实际价值和适用场景,下面会从三个角度进行归类分析。

有无状态

众所周知,React 组件有两种状态数据:一种是组件自身拥有并管理着的 state,这种数据状态一旦以参数的形似传给子组件,子组件接收到的就是另一种数据状态 —— props。props 是由父组件进行管理的 state,当然子组件也可以将接收到的 props 转化为自身的 state,也可以作为参数进行透传到孙子组件(关于数据流的详细描述,你可以查看延伸阅读)。所以根据组件是否包含自身的数据状态可以将组件分为:

  • 状态组件:顾名思义就是拥有并管理着属于自己 state 的组件。这类组件一般和具体的业务逻辑有关联,在实际的项目场景中用到的比较多。在上面的组件创建方式中,除了 Stateless Component 外,其它方式都可以创建状态组件。
  • 无状态组件:相比状态组件,无状态组件(Stateless Component)就很简单了,它接受父组件传递的 props,没有属于自己的 state,而且执行过程中的 this 不指向组件本身,以至其无法使用组件实例相关的方法以及生命周期方法。但是正因为它的极其简单性,导致了它的极其强大和高可用性,同时导致了它不可优化(这一点在支持 Hooks 的 Functional Component 中已经得到解决)。

职责分开

在实际的业务场景中,组件扮演着至关重要的角色。组件通常将逻辑和表现杂合在一起。

  • 逻辑:一般是指与 UI 无关的东西,例如 API 的调用、数据操作以及事件处理等。
  • 表现:是指渲染方法中创建元素用来显示 UI 的部分。

如果一个项目中包含大量糅杂了逻辑和表现的组件,可想而知该项目的维护成本之高,逻辑复用能力只差。为了提高项目的可维护性和组件逻辑的可复用性,我们需要清晰的定义逻辑和表现之间的界限,然后分离它们,形成两种不同职能的组件:

  • 容器组件:又名聪明组件,主要包含有关具体业务逻辑涉及到的一切,例如 API 的调用、数据操作以及事件处理等,并将处理好的数据状态通过 props 的形式传递给表现组件。一般容器组件都是状态组件!
  • 表现组件:又名傻瓜组件,只是包含 UI 的定义,而其数据状态主要是通过 props 的形式从容器组件接收而来。该类组件都是由无状态组件扮演!

是否受控

表单是我们收集用户信息的最常用途径,一般的表单会包含很多表单元素组件,如:input、checkbox、radio 等,它会都可以利用原生的实现方式来控制自己的状态,而不需要外界插手。而在实际的项目开发中,为了保证功能的完整性,一般会为这些组件提供默认值或者通过外部组件修改其状态的需求,这时这些组件状态的管理就需要外界插手了。为此,我们将表单元素组件分为以下两类:

  • 自由组件:组件的状态完全由自己进行全权管理的,不需要外界参与。
class Uncontrolled extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      value: ''
    }

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  handleChange(e) {
    this.setState({
      value: e.target.value
    });
  }
  
  handleSubmit(e) {
    e.preventDefault();
    console.log(this.state.value);
  }
  
  // ⚠️ 注意:input 的值完全是由自己掌握的,我们没有通过 value 进行设置,
  // 而只是通过 onChange 事件监听值的变化,然后保存变化后的值
  render() {
    return (
      <form>
        <input type="text" onChange={this.handleChange} />
        <button onClick={this.handleSubmit}>submit</button>
      </form>
    )
  }
}
复制代码
  • 受控组件:和自由组件完全自己管理状态相反,受控组件的状态是由外部通过 props 或者 state 进行控制的。
class Controlled extends React.Component {
  constructor(props) {
    super(props);
    
    this.state = {
      firstName: 'li',
      lastName: 'lane'
    }

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  handleChange({ target }) {
    this.setState({
      [target.name]: target.value
    });
  }
  
  handleSubmit(e) {
    e.preventDefault();
    
    const { firstName, lastName } = this.state;
    console.log(`fullName: ${firstName} ${lastName}`);
  }
  
  // ⚠️ 注意:和上面自由组件中数据状态完全由自己掌握相比,这里很明显通过 value 给 input 实时设置了值
  // 而值的来源则是通过 onChange 事件监听 input 的输入
  render() {
    const { firstName, lastName } = this.state;
    
    return (
      <form>
        <input
          type="text"
          name="firstName"
          value={firstName}
          onChange={this.handleChange}
        />
        <input
          type="text"
          name="lastName"
          value={lastName}
          onChange={this.handleChange}
        />
        <button onClick={this.handleSubmit}>submit</button>
      </form>
    )
  }
}
复制代码

总结

在实际的项目开发中,无状态组件和状态组件的使用场景可以分别对应到表现组件和容器组件的使用场景,根据实际的需求进行拆分和使用。对于自由组件和受控组件,由于受控组件非常灵活,可以提供很好的 API 供外部进行使用和自定义,所以使用的比较多,而自由组件则恰恰相反!

组件设计模式

image.png

断断续续使用 React 也有两年了吧,从当初的小白到现在可以独手揽项目,整体感觉挺香,但是其实也和大部分同学一样,经历过组件逻辑复用的痛。下面我们就来细数一下组件逻辑复用模式的那些年!

Mixin

虽然 React 已经对 Mixin 不再支持,但是为了追寻历史缘由,我们还是得了解一下,否则就短片了!

首先声明,Mixin 只能和 React.createClass 工厂方法搭配使用。

我们可以通过创建一个对象字面量来创建一个 Mixin,这个对象字面量可以拥有和组件同样的方法和属性,形如:

// 定义一个 Mixin
const WindowResize = {
  getInitialState() {
    return {
      innerWidth: window.innerWidth
    }
  },
    
  componentDidMount() {
    window.addEventListener('resize', this.handleResize)
  },
    
  componentWillUnmount() {
    window.removeEventListener('resize', this.handleResize)
  },
  
  handleResize() {
    this.setState({
      innerWidth: window.innerWidth
    })
  }
}

// 在组件中使用 Mixin
const MyComponent = React.createClass({
  mixins: [WindowResize],
  render() {
    console.log('window.innerWidth', this.state.innerWidth)
  }
})
复制代码


在组件中,会有一个 mixins 数组属性容纳需要注入的 Mixin。一个 Mixin 可以在多个组件中被引入,同样一个组件也可以引入多个 Mixin。说到这里是不是有种熟悉的感觉(即使你没有在 React 中使用过 Mixin),如果你用过 Vue。当然,和 Vue 中的 Mixin 一样,可以合并生命周期方法和初始化状态。有人可能会问,这么好用的特性为什么会废弃呢?下面我们就来细数 Mixin 的几宗罪:

  • 组件通信:Mixin 和组件进行通信可以通过两种方式:内部函数和状态。内部函数的实现是为了逻辑更加灵活,但是在实际的使用过程中,我们不得不去查看每个相关 Mixin 的实现,这无疑是加大了开发者的心智负担;另外由于 Mixin 是封装的逻辑,有时 Mixin 在组件中更新了特殊属性,而导致组件重新渲染,但是开发者很难控制或者定位问题。
  • 逻辑冗余:组件如果使用了多个 Mixin,但是最终需要实现不同的功能,当移除某些 Mixin 或者行为发生变化时,很难消除废弃的代码。
  • 冲突:对于这种注入形式的代码复用,冲突是必须要考虑的问题。虽然 React 可以聪明的合并生命周期方法,但是如果多个 Mixin 定义或调用了同样的函数,抑或是在状态中使用了相同的数据,那么 React 对此就无能为力了。
  • 互相依赖:如果出现了 Mixin 之间相互依赖的问题,那更是苦不堪言,这种耦合会导致组件的重构和应用的扩展变得非常困难。


当然,如果你使用过或者正在使用 Vue 开发项目,恰好你也用过 Mixin,那你肯定会有以上的担忧,甚至是你已经遇到了上面的问题了,那就不要怕,微笑着面对它吧!😄

React Mixin.png

HOC

根据上面关于 Mixin 的介绍,我们不难发现 Mixin 带来的问题似乎远远超过了它在组件间共享功能方面的用处。随着 class Component 的到来,Mixin 彻底被废弃,但是探索组件间共享功能的模式还需要继续呀!于是,社区根据高阶函数的概念,衍生除了 HOC(高阶组件)的概念。

高阶函数是这样一类函数,它们对传入的函数进行增强,并返回一个添加了额外行为的新函数。而高阶组件其实也是一个函数,只不过这个函数是接受组件作为参数,并对组件进行增强后返回,形如:

const Hoc = Component => EnhancedComponent 或者
const Hoc = args => Component => EnhancedComponent
复制代码


HOC 的定义形式有三种:

  • 属性代理:是高阶组件常见的实现方式。一般是高阶组件接受一个组件作为参数,然后返回一个新的组件。返回的新组件只是传入参数组件的代理,并在 render 方法中将参数组件渲染出来。高阶组件除了自身的逻辑处理外,其余的功能都会交给参数组件进行处理。这样通过高阶组件传递属性的方式就是属性代理。
const HOCComponent = (WrappedComponent) => {
  return class NewComponent extends React.Component {
    // do something ...
    render() {
      return (
        <WrappedComponent { ...this.props } />
      )
    }
  }
}
复制代码
  • 反向继承:又称渲染劫持,指的是高阶组件所返回的新组件继承自传入的参数组件,因为是高阶组件所返回的新组件被动继承了传入的参数组件,所有的调用都会反向,所以就被称为反向继承。
const HOCComponent = (WrappedComponent) => {
  return class NewComponent extends WrappedComponent {
    // do something ...
    render() {
      if (this.props.isLogin) {
        return super.render()
      }
      return null
    }
  }
}
复制代码
  • 高阶组件工厂函数:它旨在分离参数和组件的传入,形如:
const Hoc = args => Component => EnhancedComponent
复制代码


这种方式的使用在 React-Redux、Ant Design 等库中都有存在,而且我相信你一定用过。

更多关于 Mixin 的介绍和 HOC 的三种形式的作用和使用场景,你可以跳到到延伸阅读查看相关内容✋!

image.png

HOC 的缺点:

  • 多个 HOC 一起使用时,无法直接判断子组件的 props 是哪个 HOC 负责传递的
  • 多个组件嵌套,容易产生同样名称的props
  • HOC 可能会产生许多无用的组件,加深了组件的层级(Wrapper hell)

WX20200201-121547@2x.png

Render Props

Render Props 是一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。具有 Render Props 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。形如:

const DataProvider = (props) => {
	const { render, ...restProps } = props
  
  if (typeof render === 'function') {
    return render(...restProps)
  }
  
  return null
}

// 下面就使用了 Render Props 技术
const MyComponent = () => (
  <DataProvider render={data => (
    <h1>Hello {data.target}</h1>
  )}/>
)
复制代码


还记得 Context 吗?它也采用了 Render Props,只不过它是将需要渲染的组件函数赋值给了 chlidren 属性。

const { Provider, Consumer } = React.createContext(defaultValue)

const Parent = () => (
  <Provider value={/* some value */}>
  	<Child />
  </Provider>	
)

const Child = () => (
  <Consumer>
    {value => /* render something based on the context value */}
  </Consumer>
)
复制代码


Render Props 完全是依托于 React 组件传参极其灵活的特性而产生的。它依然不是由官方而是由社区推动的!

Render Props模式的出现主要是为了解决HOC所出现的问题。有如下优点:

  • 支持 ES6
  • 不用担心 props 命名问题,在 render 函数中只取需要的 state
  • 不会产生无用的组件加深层级
  • render props 模式的构建都是动态的,所有的改变都在 render 中触发,可以更好的利用组件内的生命周期。

Hooks

Hooks 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}
复制代码


Hooks 作为目前主流的 React 编写组件的形式,有太多的东西可以探究了,这部分具体的内容大家可以先去官网上看看。后续,我也会单独出一篇文章详细的讲解 Hooks 相关的知识,大家敬请期待!

总结

为了更好的实现组件间逻辑的复用,诞生了一个又一个的组件设计模式,这些模式的诞生的背景凝聚了多少人的智慧和汗水。每种模式都有它们实际存在的价值,我们不该以有色的眼光看待它们,而是怀揣敬仰之心。在实际的业务场景中,我们更是要再三权衡、合理探讨,而不是今天用这个,明天用那个,更不应该盲目跟从。

全文总结

全文到这里就结束了!先是讲到了 React 中组件创建方式的演进,然后是我自己认为设计高可用组件应该保证的几大原则,接着讲到了在实际的业务开发中,我们会常常接触到的业务组件分类和对比,最后讲到了 React 中为了复用组件间的逻辑而产生的一些列设计模式。虽然针对没点的讲解都很粗糙,但是至少大致的思路是理出来了,算是一个不错的开始吧。再次强调两点:技术没有好坏,只有适不适合;要想全面的了解一项技术,必须要追根溯源👏!

Q&A

1、class Component this 指向问题解决

  • 在 constructor 构造函数中手动绑定 this:

constructor 构造函数只会在组件初始化的时候执行一次,也就是在这里通过 bind 方法给函数绑定 this 后,绑定后的函数就是定值了,在 JSX 中引用的也是定值,避免了函数引用变化导致不必要的 rerender。但是有点麻烦的是:每个需要在 JSX 中引用的函数就要在 constructor 构造函数中 bind,工作比较多,而且很容易忘记。

  • 在 JSX 中引用函数时绑定 this

jsx 可以糅杂 html/css/JavaScript。我们完全可以动态绑定属性,为此我们可以通过如下方法解决 this 绑定的问题:

// 创建一个 Input 组件
export default class Input extends React.Component {
  constructor(props) {
    super(props);

    // 初始化 state
    this.state = {
      text: props.initialValue || 'placeholder'
    };

    // 为函数手动绑定 this
    // this.handleChange = this.handleChange.bind(this);
  }

  // 定义 input 的 onChange 方法
  handleChange(e) {
    this.setState({
      text: e.target.value,
    })
  },
  
  render() {
    const { text } = this.state;

    return (
      <div>
        <p>{text}</p>
        {/* 在 jsx 中动态绑定 this */}
        <input type="text" value={text} onChange={this.handleChange.bind(this)} />
      </div>
    );
  }
}

// 设置 props 传参类型
Input.propTypes = {
  initialValue: React.PropTypes.string
};

// 组件默认的props值
Input.defaultProps = {
  initialValue: ''
};
复制代码


这样的方法虽然可以解决 this 绑定的问题,但是在每次 diff 比较的时候,this.handleChange.bind(this) 都是通过动态计算的新值,也就是每次 rerender 都不一样。如果是对应的组件就会义无反顾的重新渲染。

  • 用箭头函数语法声明函数
// 创建一个 Input 组件
export default class Input extends React.Component {
  constructor(props) {
    super(props);

    // 初始化 state
    this.state = {
      text: props.initialValue || 'placeholder'
    };

    // 为函数手动绑定 this
    // this.handleChange = this.handleChange.bind(this);
  }

  // 定义 input 的 onChange 方法
  // 用箭头函数语法声明函数
  handleChange = (e) => {
    this.setState({
      text: e.target.value,
    })
  },
  
  render() {
    const { text } = this.state;

    return (
      <div>
        <p>{text}</p>
        <input type="text" value={text} onChange={this.handleChange} />
      </div>
    );
  }
}

// 设置 props 传参类型
Input.propTypes = {
  initialValue: React.PropTypes.string
};

// 组件默认的props值
Input.defaultProps = {
  initialValue: ''
};
复制代码


这个方法完全是利用了箭头函数的特性:箭头函数本身没有 this,其 this 完全是继承自它的定义上下文的。在组件中定义 this 就指向组件实例了,所以其逻辑中的 this 引用也就是组件实例,很好的解决了 this 指向的问题。再者,组件实例的初始化随着组件的挂载而进行,所以组件挂载后,不管 rerender 多少次,handleChange 都是实例化的值,也就是同一份值,很好的避免了无用的 rerender。是推荐的用法!

2、React.Component VS React.PureComponent

熟悉 React 组件的你一定会知道怎么通过生命周期方法避免无用的 rerender —— 对,那就是 shouldComponentUpdate。这个方法会接受新的 props 和新的 state,通过比较新旧 props 和 state 返回 true 或 false 来避免组件的 rerender。Component 和 PureComponent 的区别就是:PureComponent 组件已经隐式的实现了 shouldComponentUpdate 方法(浅比较),不需要我们重复实现,而 Component 则需要我们根据自己的需求进行实现。

3、Stateless Component VS Functional Component

没错!都只是声明了一个函数,但是它们之间有明显的区别:

  • FC 可以通过 Hooks 拥有并管理自己的 state,而 SC 不能拥有自己的 state 而只是简单的展示数据
  • FC 可以通过 Hooks 的依赖项和 memoize 方法优化重渲染
  • FC 还可以通过 Hooks 来模拟 class Component 里面的生命周期方法

#### 4、create-react-class 在 [React.createClass](#NPwPg) 部分,我们介绍了 React 早期的通过 ES5 语法创建组件的方式,而且已经郑重申明改动方式已经从 React 核心包中被移除。当然如果你想尝试这种方式,你可以使用 [create-react-class](https://www.npmjs.com/package/create-react-class) 这个 npm 包 —— A drop-in replacement for React.createClass。其基本的用法和普通的 npm 包一样,使用文档可以参考[这里](https://reactjs.org/docs/react-without-es6.html)!

延伸阅读

文章分类
前端
文章标签