高阶组件(Higher-Order Components)

有时候人们很喜欢造一些名字很吓人的名词,让人一听这个名词就以为本身不可能学会,从而让人望而却步。可是其实这些名词背后所表明的东西其实很简单。html

我不能说高阶组件就是这么一个东西。可是它是一个概念上很简单,但却很是经常使用、实用的东西,被大量 React.js 相关的第三方库频繁地使用。在前端的业务开发当中,你不掌握高阶组件其实也能够完成项目的开发,可是若是你可以灵活地使用高阶组件,可让你代码更加优雅,复用性、灵活性更强。它是一个加分项,并且加的分还很多。前端

本章节可能有部份内容理解起来会有难度,若是你以为没法彻底理解本节内容。能够先简单理解高阶组件的概念和做用便可,其余内容选择性地跳过。react

了解高阶组件对咱们理解各类 React.js 第三方库的原理颇有帮助。git

什么是高阶组件

高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。github

const NewComponent = higherOrderComponent(OldComponent)

重要的事情再重复一次,高阶组件是一个函数(而不是组件),它接受一个组件做为参数,返回一个新的组件。这个新的组件会使用你传给它的组件做为子组件,咱们看看一个很简单的高阶组件:ajax

import React, { Component } from 'react'

export default (WrappedComponent) => {
  class NewComponent extends Component {
    // 能够作不少自定义逻辑
    render () {
      return <WrappedComponent />
    }
  }
  return NewComponent
}

如今看来好像什么用都没有,它就是简单的构建了一个新的组件类 NewComponent,而后把传进入去的 WrappedComponent 渲染出来。可是咱们能够给 NewCompoent 作一些数据启动工做:设计模式

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 在挂载阶段从 LocalStorage 加载数据,而且 setState 到本身的 state.data 中,而渲染的时候将 state.data 经过 props.data 传给 WrappedComponent服务器

这个高阶组件有什么用呢?假设上面的代码是在 src/wrapWithLoadData.js 文件中的,咱们能够在别的地方这么用它:app

import wrapWithLoadData from './wrapWithLoadData'

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

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

假如 InputWithUserName 的功能需求是挂载的时候从 LocalStorage 里面加载 username 字段做为 <input /> 的 value 值,如今有了 wrapWithLoadData,咱们能够很容易地作到这件事情。函数

只须要定义一个很是简单的 InputWithUserName,它会把 props.data 做为 <input /> 的 value 值。然把这个组件和 'username' 传给 wrapWithLoadDatawrapWithLoadData 会返回一个新的组件,咱们用这个新的组件覆盖原来的 InputWithUserName,而后再导出去模块。

别人用这个组件的时候实际是用了被加工过的组件:

import InputWithUserName from './InputWithUserName'

class Index extends Component {
  render () {
    return (
      <div>
        用户名:<InputWithUserName />
      </div>
    )
  }
}

根据 wrapWithLoadData 的代码咱们能够知道,这个新的组件挂载的时候会先去 LocalStorage 加载数据,渲染的时候再经过 props.data 传给真正的 InputWithUserName

若是如今咱们须要另一个文本输入框组件,它也须要 LocalStorage 加载 'content' 字段的数据。咱们只须要定义一个新的 TextareaWithContent

import wrapWithLoadData from './wrapWithLoadData'

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

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

写起来很是轻松,咱们根本不须要重复写从 LocalStorage 加载数据字段的逻辑,直接用 wrapWithLoadData 包装一下就能够了。

咱们来回顾一下到底发生了什么事情,对于 InputWithUserName 和 TextareaWithContent 这两个组件来讲,它们的需求有着这么一个相同的逻辑:“挂载阶段从 LocalStorage 中加载特定字段数据”。

若是按照以前的作法,咱们须要给它们两个都加上 componentWillMount 生命周期,而后在里面调用 LocalStorage。要是有第三个组件也有这样的加载逻辑,我又得写一遍这样的逻辑。但有了 wrapWithLoadData 高阶组件,咱们把这样的逻辑用一个组件包裹了起来,而且经过给高阶组件传入 name 来达到不一样字段的数据加载。充分复用了逻辑代码。

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

高阶组件的灵活性

代码复用的方法、形式有不少种,你能够用类继承来作到代码复用,也能够分离模块的方式。可是高阶组件这种方式颇有意思,也很灵活。学过设计模式的同窗其实应该能反应过来,它其实就是设计模式里面的装饰者模式。它经过组合的方式达到很高的灵活程度。

假设如今咱们需求变化了,如今要的是经过 Ajax 加载数据而不是从 LocalStorage 加载数据。咱们只须要新建一个 wrapWithAjaxData 高阶组件:

import React, { Component } from 'react'

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

    componentWillMount () {
      ajax.get('/data/' + name, (data) => {
        this.setState({ data })
      })
    }

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

其实就是改了一下 wrapWithLoadData 的 componentWillMount 中的逻辑,改为了从服务器加载数据。如今只须要把 InputWithUserName 稍微改一下:

import wrapWithAjaxData from './wrapWithAjaxData'

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

InputWithUserName = wrapWithAjaxData(InputWithUserName, 'username')
export default InputWithUserName

只要改一下包装的高阶组件就能够达到须要的效果。并且咱们并无改动 InputWithUserName 组件内部的任何逻辑,也没有改动 Index 的任何逻辑,只是改动了中间的高阶组件函数。

(如下内容为选读内容,有兴趣的同窗能够继续往下读,不然也能够直接跳到文末的总结部分。)

多层高阶组件(选读)

假如如今需求有变化了:咱们须要先从 LocalStorage 中加载数据,再用这个数据去服务器取数据。咱们改一下(或者新建一个)wrapWithAjaxData 高阶组件,修改其中的 componentWillMount

...
    componentWillMount () {
      ajax.get('/data/' + this.props.data, (data) => {
        this.setState({ data })
      })
    }
...

它会用传进来的 props.data 去服务器取数据。这时候修改 InputWithUserName

import wrapWithLoadData from './wrapWithLoadData'
import wrapWithAjaxData from './wrapWithAjaxData'

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

InputWithUserName = wrapWithAjaxData(InputWithUserName)
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

你们能够看到,咱们给 InputWithUserName 应用了两种高阶组件:先用 wrapWithAjaxData 包裹 InputWithUserName,再用 wrapWithLoadData 包含上次包裹的结果。它们的关系就以下图的三个圆圈:

实际上最终获得的组件会先去 LocalStorage 取数据,而后经过 props.data 传给下一层组件,下一层用这个 props.data 经过 Ajax 去服务端取数据,而后再经过 props.data 把数据传给下一层,也就是 InputWithUserName。你们能够体会一下下图尖头表明的组件之间的数据流向:

用高阶组件改造评论功能(选读)

你们对这种在挂载阶段从 LocalStorage 加载数据的模式都很熟悉,在上一阶段的实战中,CommentInput 和 CommentApp 都用了这种方式加载、保存数据。实际上咱们能够构建一个高阶组件把它们的相同的逻辑抽离出来,构建一个高阶组件 wrapWithLoadData

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

    componentWillMount () {
      let data = localStorage.getItem(name)
      try {
        // 尝试把它解析成 JSON 对象
        this.setState({ data: JSON.parse(data) })
      } catch (e) {
        // 若是出错了就当普通字符串读取
        this.setState({ data })
      }
    }

    saveData (data) {
      try {
        // 尝试把它解析成 JSON 字符串
        localStorage.setItem(name, JSON.stringify(data))
      } catch (e) {
        // 若是出错了就当普通字符串保存
        localStorage.setItem(name, `${data}`)
      }
    }

    render () {
      return (
        <WrappedComponent
          data={this.state.data}
          saveData={this.saveData.bind(this)}
          // 这里的意思是把其余的参数原封不动地传递给被包装的组件
          {...this.props} />
      )
    }
  }
  return LocalStorageActions
}

CommentApp 能够这样使用:

class CommentApp extends Component {
  static propTypes = {
    data: PropTypes.any,
    saveData: PropTypes.func.isRequired
  }

  constructor (props) {
    super(props)
    this.state = { comments: props.data }
  }

  handleSubmitComment (comment) {
    if (!comment) return
    if (!comment.username) return alert('请输入用户名')
    if (!comment.content) return alert('请输入评论内容')
    const comments = this.state.comments
    comments.push(comment)
    this.setState({ comments })
    this.props.saveData(comments)
  }

  handleDeleteComment (index) {
    const comments = this.state.comments
    comments.splice(index, 1)
    this.setState({ comments })
    this.props.saveData(comments)
  }

  render() {
    return (
      <div className='wrapper'>
        <CommentInput onSubmit={this.handleSubmitComment.bind(this)} />
        <CommentList
          comments={this.state.comments}
          onDeleteComment={this.handleDeleteComment.bind(this)} />
      </div>
    )
  }
}

CommentApp = wrapWithLoadData(CommentApp, 'comments')
export default CommentApp

一样地能够在 CommentInput 中使用 wrapWithLoadData,这里就不贴代码了。有兴趣的同窗能够查看高阶组件重构的 CommentApp 版本

总结

高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。新的组件使用传入的组件做为子组件。

高阶组件的做用是用于代码复用,能够把组件之间可复用的代码、逻辑抽离到高阶组件当中。新的组件和传入的组件经过 props 传递信息。

高阶组件有助于提升咱们代码的灵活性,逻辑的复用性。灵活和熟练地掌握高阶组件的用法须要经验的积累还有长时间的思考和练习,若是你以为本章节的内容没法彻底消化和掌握也没有关系,能够先简单了解高阶组件的定义、形式和做用便可。

相关文章
相关标签/搜索