一、定义
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数,如:
const NewComponent = higherOrderComponent(OldComponent);
二、例子
高阶组件是一个函数(而不是组件),它接受一个组件作为参数,返回一个新的组件。这个新的组件会使用你传给它的组件作为子组件,我们可以看个例子来进一步理解一下。
- 假设在src/wrapWithLoadData.js 文件中写一个HOC,要求NewComponent 会根据第二个参数 name 在挂载阶段从 localStorage 加载数据,并且 setState 到自己的 state.data 中,而渲染的时候将 state.data 通过 props.data 传给 WrappedComponent,如下:
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;
}
- 假如有一个组件的需求是挂载的时候从 localStorage 里面加载 username 字段作为 <input /> 的 value 值,现在有了 wrapWithLoadData,我们可以很容易地做到这件事情。只需要定义一个非常简单的 InputWithUserName,它会把 props.data 作为 <input /> 的 value 值。然把这个组件和 'username' 传给 wrapWithLoadData,wrapWithLoadData 会返回一个新的组件,我们用这个新的组件覆盖原来的 InputWithUserName,然后再导出去模块,我们可以在src/inputWithUserName.js 文件中这样写:
import wrapWithLoadData from './wrapWithLoadData';
class InputWithUserName extends Component {
render () {
return <input value={this.props.data} />
}
}
InputWithUserName = wrapWithLoadData(InputWithUserName, 'username');
export default InputWithUserName;
- 这个新的组件挂载的时候会先去 localStorage 加载数据,渲染的时候再通过 props.data 传给真正的 InputWithUserName,别人用这个组件的时候实际是用了被加工过的组件:
import InputWithUserName from './InputWithUserName';
class Index extends Component {
render () {
return (
<div>
用户名:<InputWithUserName />
</div>
)
}
}
- 如果现在我们需要另外一个文本输入框组件,它也需要 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 传递数据。
三、高阶组件的灵活性
代码复用的方法、形式有很多种,可以用类继承来做到代码复用,也可以用分离模块的方式。但是高阶组件这种方式很有意思,也很灵活,它其实就是设计模式里面的装饰者模式,通过组合的方式达到很高的灵活程度。
假如现在需求变成了:需要先从 localStorage 中加载数据,再用这个数据去服务器取数据。
- 我们可以在src/wrapWithAjaxData.js 文件中写一个wrapWithAjaxData 高阶组件:
import React, { Component } from 'react';
import ajax from 'Ajax';
export default (WrappedComponent, name) => {
class NewComponent extends Component {
constructor () {
super()
this.state = { data: null }
}
componentWillMount () {
ajax.get('/data/' + this.props.data, (data) => {
this.setState({ data })
})
}
render () {
return <WrappedComponent data={this.state.data} />
}
}
return NewComponent;
}
- 它会用传进来的 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。组件之间的数据流向如下图:
四、总结
- 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。新的组件使用传入的组件作为子组件。
- 高阶组件的作用是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。新的组件和传入的组件通过 props 传递信息。
- 高阶组件有助于提高我们代码的灵活性,逻辑的复用性。
网友评论