React 进阶

高阶组件 (1/2)

Posted by Ericteen on August 1, 2017

高阶组件

高阶组件是一个概念上很简单,却十分实用的东西,被大量的 React 相关的第三方库频繁使用。在前端业务开发当中,灵活使用高阶组件可以让你的代码更为优雅,灵活性和复用性也会更强。同时,了解高阶组件对我们理解各种 React 第三方库也有很大的帮助,例如 Redux 的 connect 和 Relay 的 createContainer

什么是高阶组件

A higher-order component is a function that takes a component and returns a new component. 高阶组件是一个函数,接收一个组件并返回一个新的组件。

const EnhancedComponent = highOrderComponent(WrappedComponent)

在 React 中,高阶组件是一种复用组件逻辑的技术。它是一个函数而非组件,就像一个组件将 props 转换为 UI,高阶组件接收一个组件作为参数,返回一个新的组件。

进一步学习之前,我们先回顾一下 new 和构造函数之间的关系。

var Person = function(name, age) {
  this.name = name;
  this.age = age;
  this.getName = function(){
    return this.name;
  }
}

function New(fn) {
  var res = {};
  if (fn.prototype !== null) {
    res.__proto__ = fn.prototype;
  }

  var ret = fn.apply(res, Array.prototype.slice.call(arguments, 1));

  if ((typeof ret === 'object || typeof ret === 'function) && ret !== null) {
    return ret;
  }

  return res;
}

var p1 = New(Person, 'tom', 20);
console.log(p1.getName());

console.log(p1 instanceof Person); // true

在以上的例子中,我定义了一个本质上与普通函数没区别的构造函数,然后将该构造函数以参数形式传入 New 函数中。我在 New 函数中进行了一些的逻辑处理,让 New 函数的返回值为一个实例,正因为 New 的内部逻辑,让构造函数中的 this 能够指向返回的实例。

高阶组件的实例

同理,我来写一个简单的高阶组件。

import React, { Component } from 'react'

export default (WrappedComponent) => {
  class NewComponent extends Component {
    // 可以做很多自定义逻辑
    render () {
      return <WrappedComponent />
    }
  }
  return NewComponent
}

上例简单构建了一个新的组件类 NewComponent,然后把传入的 WrappedComponent 组件渲染出来。接下来我给 NewComponent 添加一些功能。

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      let data = localStorage.getItem(name)
      this.setState({ data })
    }

    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

增加功能之后,NewComponent 会根据第二个参数 name 在挂载阶段从 LocaleStorage 加载数据,并且添加到自己的 state.data 中,而渲染的时候将数据通过 props 传送给 WrappedComponent 中。

我们来复用这段代码,假设上述代码在 src/wrapLoadData.js 文件中。

import React, {Component} from 'react'
import wrapLoadData from './wrapLoadData'

class InputWithUserName extends Component {
  render() {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapLoadData(InputWithUserName, 'username')

export default InputWithUserName

假如 InputWithUserName 的功能需求是挂载的时候从 LocalStorage 里面加载 username 字段作为 <input />value 值,现在有了 wrapLoadData,我们可以很容易地做到这件事情。

根据 wrapLoadData 的代码我们可以知道,这个新的组件挂载的时候会先去 LocalStorage 加载数据,渲染的时候再通过 props.data 传给真正的 InputWithUserName

如果现在我们需要另外一个文本输入框组件,它也需要 LocalStorage 加载 ‘content’ 字段的数据。我们只需要定义一个新的 TextareaWithContent

import wrapLoadData from './wrapLoadData'

class TextareaWithContent extends Component {
  render () {
    return <textarea value={this.props.data} />
  }
}

TextareaWithContent = wrapWoadData(TextareaWithContent, 'content')
export default TextareaWithContent

这样做起来会非常轻松,我们根本不需要重复写从 LocalStorage 加载数据字段的逻辑,直接用 wrapWithLoadData 包装一下就可以了。

总结

上述两个组件 InputWithValueTextareaWithContent 的需求都有一个相同的逻辑,即挂载阶段从 LocalStorage 中加载特定的字段数据

高阶组件的作用,其实就是为了组件之间的代码复用。组件可能有着某些相同的逻辑,把这些逻辑抽离出来,放到高阶组件中进行复用。高阶组件内部的包装组件和被包装组件之间通过 props 传递数据