有时候人们很喜欢造一些名字很吓人的名词,让人一听这个名词就以为本身不可能学会,从而让人望而却步。可是其实这些名词背后所表明的东西其实很简单。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'
传给 wrapWithLoadData
,wrapWithLoadData
会返回一个新的组件,咱们用这个新的组件覆盖原来的 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
传递信息。
高阶组件有助于提升咱们代码的灵活性,逻辑的复用性。灵活和熟练地掌握高阶组件的用法须要经验的积累还有长时间的思考和练习,若是你以为本章节的内容没法彻底消化和掌握也没有关系,能够先简单了解高阶组件的定义、形式和做用便可。