以一个项目中实际的例子,介绍如何使用React插槽和PureComponent优化你的React代码

642 阅读6分钟

使用过Vue小伙伴肯定对slot插槽这个概念并不陌生,其实,react也提供了类似Vue的插槽特性,也就是render props,这篇文章将结合一个实际的例子,来介绍如何使用react的“插槽”优化我们的代码,并结合PureComponent进行性能优化。

1. 案例介绍

这是一个很典型的前端页面,主要功能是用户录入一些信息,之后进行提交,主要功能和模块如下:

  1. 页面有四个主要模块:页面的名字,右上角的提交按钮,两个展示不同类型信息的卡片
  2. 卡片1包括卡片名称,一个输入框,几个单选框,几个复选框,一个用于选择的按钮,例如点击按钮,会出现一个选择器,用户从中选择一些信息
  3. 卡片2包括卡片名称,一段文本,一个文本输入。这里的文本和卡片1的单选框有联动关系,文本会根据单选框的不同选择内容,展示不同的文字
  4. 点击页面右上角的按钮,会将两个卡片中填写的信息进行提交

下面就集中实现方案进行介绍,并评估优劣。

2. 基础版:一把梭

基础版的实现方案很简单,就是把所有的ui和逻辑写到一个文件中,下面使用类组件实现

class Page extends React.Component {
  state = { ... }

  // input处理函数
  onInputChange = () => { ... }

  // 单选框处理函数
  onRadioChange = () => { ... }

  // 复选框处理函数
  onCheckoutboxChange = () => { ... }

  // 按钮点击处理函数
  onButtonClick = () => { ... }

  // 文本输入处理函数
  onTextAreaChange = () => { ... }

  // 提交数据
  submit = () => { ... }

  // 渲染头部
  renderNavigator = () => { ... }

  // 渲染卡片1
  renderCard1 = () => { ... }

  // 渲染卡片2
  renderCard2 = () => { ... }

  // 主渲染函数
  render = () => {
    return (
      <>
        {this.renderNavigator()}
        {this.renderCard1()}
        {this.renderCard2()}
      </>
    )
  }
}

这种写法的缺点很明显,首先,所有的代码写到同一个文件中,文件太大,如果页面比较复杂, 代码行数很可能会破1000行,代码可读性差,不易扩展,相信维护过这种页面同学肯定十分痛苦。第二个原因,这个代码的性能很差。例如用户改变了文本框的内容,会执行setState,由于所有的ui都写在了一个组件内,所以整个页面都会重新渲染,但是实际发生改变的只有输入框,这就导致了不必要的渲染,性能不好。

3.升级版1.0

简单的从UI来看,这个页面可以分为三个大部分,头部的导航部分,卡片1和卡片2,因此可以把这三部分抽成三个组件,Header,Card1和Card2,下面看代码

class Header extends React.PureComponent {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <div>
        <span>{this.props.title}</span>
      	<button onClick=(this.props.onSubmit)>{this.props.btnText}</button>
      </div>
    )
  }
}
class Card1 extends React.PureComponent {
  constructor(props) {
    super(props)
  }

  renderTitle = () => { ... }

  renderInput = () => { ... }

  renderRadios = () => { ... }

  renderCheckboxs = () => { ... }

  renderButton = () => { ... }

  render() {
    return (
      <div>
          {this.renderTitle()}
          {this.renderInput()}
          {this.renderRadios()}
          {this.renderCheckboxs()}
          {this.renderButton()}
      </div>
    )
  }
}
class Card2 extends React.PureComponent {
  constructor(props) {
    super(props)
  }

  renderTitle = () => { ... }

  renderText = () => { ... }

  renderTextArea = () => { ... }

  render() {
    return (
      <div>
          {this.renderTitle()}
          {this.renderText()}
          {this.renderTextArea()}
      </div>
    )
  }
}
class Page extends React.Component {
  constructor(props) {
    super(props)
    this.state = { ... }
  }

  // ... event handlers

  render() {
    return (
      <div>
        <Header />
        <Card1 />
        <Card2 />
      </div>
    )
  }
}

这种方式通过UI将页面分为了三个组件,并且每个组件使用PureComponent,这样,如果Card1组件更新,Card2和Header组件的props没有变化,就不会更新在一定程度上优化了性能。但是,这种方式依然存在问题:

  1. 优化了性能,但没有完全优化。组件的拆分力度较大,组件更新时依然会有不必要的渲染。例如用户修改了input的内容,Card1组件会重新渲染,但是单选和多选框是不需要更新的。
  2. 对项目的后续维护人员来说,组件是一个黑箱。如果只看Page组件的话,是无法获知每个子组件的渲染内容的,并且每个组件的props会很多。由于点击提交按钮时要提交所有的信息,因此,state必须写在Page组件中,不能写在子组件里面。因此,每个组件都要接受很多的props,包括用于数据展示的state和事件处理函数。例如Card1组件里面包含了input,radio,checkout。因此需要给这个组件传递input的state和handler,radio的state和handler,checkbox的state和handler。在不了解Card1组件代码的前提下,这些props会给维护人员造成困惑。
  3. 可能会有同学将Card1和Card2组件,根据UI继续拆分,例如
class Card1 extends React.PureComponent {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <div>
        <Input />
        <Radios />
        <Checkboxs />
        <Button />
      </div>
    )
  }
}

这种写法相对于上面的写法来说,进一步缩小了组件的粒度,如果Input等子组用PureComponent实现,可以进一步减少非必要的render。但是前面提到的问题2并没有得到解决,此外,这种写法加深了组件树的层级,导致props需要多传一层,反而加重了问题2。有些同学可能想到使用Context,但是使用Context很容易导致整个组件树的更新,性能不好。

4.升级版2.0

难道就没有一种方法,既能优化性能,又能让组件层级一目了然的方法吗?答案就是render props。render props允许用户传递一个方法或者组件作为props,并在组件中渲染该组件。

个人感觉React并不是不支持插槽,jsx支持用户传递一个返回组件的函数,或者直接传递一个组件,用户可以自由地决定这个prop如何使用。Vue只是将这种比较特殊的prop进行了一种封装,独立出一个功能,叫做插槽。两者在基础使用上本有什么本质的区别。

就像Vue将prop,事件处理函数做了单独的封装,并使用不同的语法,React使用的jsx可以传递任意类型的数据,用户自行决定这个prop是什么。

React提供了一种比较特殊的render props,就是children,在代码中可以直接使用this.props.children来调用,下面就是用children继续对代码进行优化。

class Page extends React.Component {
  constructor(props) {
    super(props)
    this.state = { ... }
  }

  // ... event handlers

  render() {
    return (
      <div>
        <Header>
          <Title />
          <Button />
      	</Header>
      	<Card>
          <Input />
          <Radios />
          <Checkboxs />
          <Button />
      	</Card>
      	<Card>
          <Text />
          <TextArea />
      	</Card>
      </div>
    )
  }
}

以Card组件为例

class Card extends React.PureComponent {
  constructor(props) {
    super(props)
  }

  render() {
    <div>
      {this.props.children}
    </di>
  }
}

使用children,我们将所有子组件的渲染都写在了Page.js中,这样,就无须一层层的透传props,而且组件层级清晰,每个组件只接受本组件需要的props,一目了然。并且,每个子组件使用PureComponent实现,从而减少不必要的渲染。这种方法还可以更大程度的进行组件复用,例如Card组件就进行了复用。此外,这种方式无需考虑兄弟组件通信的问题,因为所有的state都提升到了公共的父组件当中。所有的子组件都是无状态组件,数据流清晰。

5.总结

灵活使用React的“插槽”机制,对组件进行细粒度划分,能够明确组件的层级,结合PureComponent,可以减少组件不必要的渲染。