HOC
高级组件在React
是一种比较先进的重用组件的方法,高级组件不是React
的API
,它是一种从React
的组件方式中合并而成的一种模式,所以可以说HOC
不是组件,而是一个处理模式
准确地来说,HOC
组件是一个处理组件并且返回新组件的函数,但是这个函数又是一个纯函数,遵循函数式编程规范。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
就是说,HOC
就是将一个组件变为另一个组件。
HOC
在第三方React
库中是非常普遍的,比如Redux
的connect
和Relay中的createContainer
,这两个函数后面会提到一点他们的大概实现方式和使用形式。
接下来,将讨论的是HOC
的用处和怎么去实现它
1.构造联系
之前我提到的mixins
就是一种构造父子关系方式,但是mixins
会存在一定程度坏处。所以我们有必要用另外一种方式来处理这种关系。
组件是React
复用代码的基本单位,然而,你会发现一些实现的方式并不适合传统的组件。
如下:
class CommentList extends React.Component {
constructor() {
super();
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource"是一个全局的数据处理类,用来处理数据的,可以不要在意其中的细节
comments: DataSource.getComments()
};
}
componentDidMount() {
//监听数据变化
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// 清除监听事件
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// 当数据发生变化时触发事件
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
这其中的DataSource用到了观察者模式,在组件中进行事件的订阅。我们再看下面这份代码
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
两份代码相似但是有不同,他们最终渲染出的结果是不同的,但是有三点是相同的
在挂载时,给
DataSource
监听了一个事件在事件内部,如果
DataSource
内部的数据发生变化的时候,会调用setState
来进行更新在卸载时,会移除掉监听在
DataSource上
的事件
可以想象的是,当我们构建大型的应用程序的时候,通过订阅DataSource
然后通过setState
去更新状态,这些步骤我们都可以抽象出来,形成一个函数,然后在多个组件中分享使用,这就是高级组件的用法。
我们可以写一个创建组件的函数,像CommentList和blogPost那样去订阅DataSource。这个函数接受一个组件作为参数来监听事件。
如下:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
});
其中第一个参数是一个组件,第二参数则是我们在DataSource
数据进行改变时需要执行的操作。
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
//返回一个新构建的组件
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
//进行事件绑定
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
这个HOC
不会对输入的组件做任何修改,也不做任何继承赋值的行为,它就相当于是在原来的组件的基础上进行扩展,这就是用函数式编程的好处,一般都说是纯函数零副作用。
HOC
并不知道你干了什么,它只是调用你想要调用的回调函数,然后进行数据处理,没有直接跟数据进行交互处理,它根本不担心你干了什么,你的数据从哪里来。
从widthSubscription
大体上看,这是一个普通的不能再普通的函数,我们可以自行的为这函数进行扩充,比如:为内部存取的数据,换一个不名字,不用data
而用自己特定的,又或是,可以增加shouldComponentUpdate
来实现对一些组件不执行更新等等,都是可以的。
2.不要试图修改原始的组件,使用组合方法
在HOC
中拒绝去修改原始数据
function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
// 对原组件进行的扩展,修改了原组件
return InputComponent;
}
const EnhancedComponent = logProps(InputComponent);
上面的代码就存在几个非常明显的问题了,一个就是原组件在函数中进行改变然后返回,更关键的就是,如果我们的组件中已经写好了一个componentWillReceiveProps
函数处理,但是在logProps
会被覆盖掉,很明显这不是我们想要的。
所以要避免这种事情,我们就可以采用上面已经提到的包容组件的方式,就是重新创建一个新的组件,让这个组件包裹传递进来的组件,而传递进来的组件则在render
中渲染出去。
function logProps(WrappedComponent) {
return class extends React.Component {
componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
}
上面的代码就变得非常正确了。
3.通过包容组件来处理props
不相关
就之前的说到的东西,我们可以知道,HOC
模式给组件进行扩展的方式是,通过一个临时的组件将该组件包括起来然后返回出去
包容组件和被包容的组件通过一个props
来进行传递,这一步通常是在render
中。
render() {
// 分离出额外的属性
const { extraProp, ...passThroughProps } = this.props;
// 执行的函数
const injectedProp = someStateOrInstanceMethod;
// 包裹组件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
4.最大化可组合性
不是所有的HOC
都会接受一个回调函数进行处理,有时只有一个参数,就是要被包裹的组件
const NavbarWithRouter = withRouter(Navbar);
又或者是如同Relay.createContainer
用一个config
来进行配置
const CommentWithRelay = Relay.createContainer(Comment, config);
又或者是Redux
的connect
函数
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
如果上面看不懂,可以看下面的分步:
const enhance = connect(commentListSelector, commentListActions);
const ConnectedComment = enhance(CommentList);
connect
就是一个高阶函数,返回的就是一个高阶组件,但是这也许让人疑惑,但是这是一个好方式,通过调用不断将需要的参数传递进去,内部是函数式编程规范进行处理的。
建议大家可以好好的学习一下函数式编程。
这里还有很多实用方法,之后会更新,个人还不是特别会,需要不断实践,才能更新。
下一篇将讲
React
的高级运用