React 深刻系列6:高阶组件

React 深刻系列,深刻讲解了React中的重点概念、特性和模式等,旨在帮助你们加深对React的理解,以及在项目中更加灵活地使用React。

1. 基本概念

高阶组件是React 中一个很重要且比较复杂的概念,高阶组件在不少第三方库(如Redux)中都被常用。在项目中用好高阶组件,能够显著提升代码质量。html

高阶组件的定义类比于高阶函数的定义。高阶函数接收函数做为参数,而且返回值也是一个函数。相似的,高阶组件接收React组件做为参数,而且返回一个新的React组件。高阶组件本质上也是一个函数,并非一个组件,这一点必定不要弄错。react

2. 应用场景

为何React引入高阶组件的概念?它到底有何威力?让咱们先经过一个简单的例子说明一下。git

假设有一个组件MyComponent,须要从LocalStorage中获取数据,而后渲染数据到界面。咱们能够这样写组件代码:github

import React, { Component } from 'react'

class MyComponent extends Component {

  componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
  }
  
  render() {
    return <div>{this.state.data}</div>
  }
}

代码很简单,但当有其余组件也须要从LocalStorage中获取一样的数据展现出来时,须要在每一个组件都重复componentWillMount中的代码,这显然是很冗余的。下面让咱们来看看使用高阶组件能够怎么改写这部分代码。redux

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
        this.setState({data});
    }
    
    render() {
      // 经过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)

withPersistentData就是一个高阶组件,它返回一个新的组件,在新组件的componentWillMount中统一处理从LocalStorage中获取数据的逻辑,而后将获取到的数据以属性的方式传递给被包装的组件WrappedComponent,这样在WrappedComponent中就能够直接使用this.props.data获取须要展现的数据了,如MyComponent2所示。当有其余的组件也须要这段逻辑时,继续使用withPersistentData这个高阶组件包装这些组件就能够了。设计模式

经过这个例子,能够看出高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用。高阶组件的这种实现方式,本质上是一个装饰者设计模式。app

高阶组件的参数并不是只能是一个组件,它还能够接收其余参数。例如,组件MyComponent3须要从LocalStorage中获取key等于name的数据,而不是上面例子中写死的key等于data的数据,withPersistentData这个高阶组件就不知足咱们的需求了。咱们可让它接收额外的一个参数,来决定从LocalStorage中获取哪一个数据:函数

import React, { Component } from 'react'

function withPersistentData(WrappedComponent, key) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }
    
    render() {
      // 经过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其余逻辑...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其余逻辑...
}

const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data');
const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');

新版本的withPersistentData就知足咱们获取不一样key的值的需求了。高阶组件中的参数固然也能够是函数,咱们将在下一节进一步说明。工具

3. 进阶用法

高阶组件最多见的函数签名形式是这样的:this

HOC([param])([WrappedComponent])

用这种形式改写withPersistentData,以下:

import React, { Component } from 'react'

const withPersistentData = (key) => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }
    
    render() {
      // 经过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其余逻辑...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
  
  //省略其余逻辑...
}

const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2);
const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);

实际上,此时的withPersistentData和咱们最初对高阶组件的定义已经不一样。它已经变成了一个高阶函数,但这个高阶函数的返回值是一个高阶组件。HOC([param])([WrappedComponent])这种形式中,HOC([param])才是真正的高阶组件,咱们能够把它当作高阶组件的变种形式。这种形式的高阶组件因其特有的便利性——结构清晰(普通参数和被包裹组件分离)、易于组合,大量出如今第三方库中。如react-redux中的connect就是一个典型。connect的定义以下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])(WrappedComponent)

这个函数会将一个React组件链接到Redux 的 store。在链接的过程当中,connect经过函数类型的参数mapStateToProps,从全局store中取出当前组件须要的state,并把state转化成当前组件的props;同时经过函数类型的参数mapDispatchToProps,把当前组件用到的Redux的action creators,以props的方式传递给当前组件。

例如,咱们把组件ComponentA链接到Redux上的写法相似于:

const ConnectedComponentA = connect(mapStateToProps, mapDispatchToProps)(ComponentA);

咱们能够把它拆分来看:

// connect 是一个函数,返回值enhance也是一个函数
const enhance = connect(mapStateToProps, mapDispatchToProps);
// enhance是一个高阶组件
const ConnectedComponentA = enhance(ComponentA);

当多个函数的输出和它的输入类型相同时,这些函数是很容易组合到一块儿使用的。例如,有f,g,h三个高阶组件,都只接受一个组件做为参数,因而咱们能够很方便的嵌套使用它们:f( g( h(WrappedComponent) ) )。这里能够有一个例外,即最内层的高阶组件h能够有多个参数,但其余高阶组件必须只能接收一个参数,只有这样才能保证内层的函数返回值和外层的函数参数数量一致(都只有1个)。

例如咱们将connect和另外一个打印日志的高阶组件withLog联合使用:

const ConnectedComponentA = connect(mapStateToProps)(withLog(ComponentA));

这里咱们定义一个工具函数:compose(...functions),调用compose(f, g, h) 等价于 (...args) => f(g(h(...args)))。用compose函数咱们能够把高阶组件嵌套的写法打平:

const enhance = compose(
  connect(mapStateToProps),
  withLog
);
const ConnectedComponentA = enhance(ComponentA);

像Redux等不少第三方库都提供了compose的实现,compose结合高阶组件使用,能够显著提升代码的可读性和逻辑的清晰度。

4.与父组件区别

有些同窗可能会以为高阶组件有些相似父组件的使用。例如,咱们彻底能够把高阶组件中的逻辑放到一个父组件中去执行,执行完成的结果再传递给子组件。从逻辑的执行流程上来看,高阶组件确实和父组件比较相像,可是高阶组件强调的是逻辑的抽象。高阶组件是一个函数,函数关注的是逻辑;父组件是一个组件,组件主要关注的是UI/DOM。若是逻辑是与DOM直接相关的,那么这部分逻辑适合放到父组件中实现;若是逻辑是与DOM不直接相关的,那么这部分逻辑适合使用高阶组件抽象,如数据校验、请求发送等。

5. 注意事项

1)不要在组件的render方法中使用高阶组件,尽可能也不要在组件的其余生命周期方法中使用高阶组件。由于高阶组件每次都会返回一个新的组件,在render中使用会致使每次渲染出来的组件都不相等(===),因而每次render,组件都会卸载(unmount),而后从新挂载(mount),既影响了效率,又丢失了组件及其子组件的状态。高阶组件最适合使用的地方是在组件定义的外部,这样就不会受到组件生命周期的影响了。

2)若是须要使用被包装组件的静态方法,那么必须手动拷贝这些静态方法。由于高阶组件返回的新组件,是不包含被包装组件的静态方法。hoist-non-react-statics能够帮助咱们方便的拷贝组件全部的自定义静态方法。有兴趣的同窗能够自行了解。

3)Refs不会被传递给被包装组件。尽管在定义高阶组件时,咱们会把全部的属性都传递给被包装组件,可是ref并不会传递给被包装组件。若是你在高阶组件的返回组件中定义了ref,那么它指向的是这个返回的新组件,而不是内部被包装的组件。若是你但愿获取被包装组件的引用,你能够把ref的回调函数定义成一个普通属性(给它一个ref之外的名字)。下面的例子就用inputRef这个属性名代替了常规的ref命名:

function FocusInput({ inputRef, ...rest }) {
  return <input ref={inputRef} {...rest} />;
}

//enhance 是一个高阶组件
const EnhanceInput = enhance(FocusInput);

// 在一个组件的render方法中...
return (<EnhanceInput 
  inputRef={(input) => {
    this.input = input
  }
}>)

// 让FocusInput自动获取焦点
this.input.focus();

下篇预告:

React 深刻系列7:React 经常使用模式


个人新书《React进阶之路》已上市,请你们多多支持!
连接:京东 当当

图片描述

相关文章
相关标签/搜索