高阶组件
高阶组件是一个函数,传入一个源组件,并且返回一个包装后的组件。高阶组件用于强化源组件功能,并且分离源组件和包装组件,使代码能够复用。是装饰器模式的一种实现。
HOC经常应用在React 的一些第三方库中,比如Redux的connect 和Relay的createFragmentContainer
将HOC应用于横切关注点(Cross-Cutting Concerns)
横切关注点
横切关注点(cross-cutting concern)是指在面向切面(或方向)的编程中,在应用程序中影响多个模块的代码块,但是这些代码块又不能清晰的从模块中分离出来,导致代码重复。面向过程和函数式编程是是整个的过程调用,不能同时实现两个目标(要实现的功能和相关的横切关注点)。面向切面的编程旨在将横切关注点封装到方面中以保持模块化。这允许对解决横切问题的代码进行干净的隔离和重用。通过基于横切关注点的设计,使代码模块化和简化代码的维护,软件工程设计的更好。
例如,如果写一个处理医疗记录的应用,医疗记录是一个核心的关乎点,然而,把更改医疗记录的日志写到数据库,或是认证系统是横切关注点,这些横切关注点与英勇的其他许多模块交互。
组件是React中代码重用的主要单元。然而,您会发现有些场景并不直接适合传统组件。
例子
假如有一个评论列表CommentList组件。订阅一个外部数据源的改变,数据源更新数据后刷新评论列表:
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
稍后,您编写了一个组件来订阅单个博客文章,它遵循类似的模式:
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} />;
}
}
CommentList和BlogPost并不完全相同——它们在DataSource上调用不同的方法,并且它们呈现不同的输出。但它们的大部分实现都是一样的:
- 在挂载时,添加一个DataSource监听器。
- 在监听听器内部,只要数据源发生更改,就调用setState。
- 卸载时,删除监听器。
你可以想象,在一个大的应用程序中,订阅DataSource和调用setState的相同代码会一次又一次地发生。我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。这就是高阶组件的优势所在。 我们定义一个函数withSubscription,函数的第一个参数为源组件,第二个参数为源组件获取订阅的数据类型函数。
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
当CommentListWithSubscription和BlogPostWithSubscription被渲染时,把DataSource对应的值传递给CommentList和BlogPost。
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
注意,HOC不修改源组件,也不使用继承来复制其行为。相反,HOC通过将源组件包装在容器组件中来组合源组件。HOC是一个没有副作用的纯函数。 源组件接收所有的包装组件的props,还有包装组件用于渲染输出一些新的prop data。HOC不用关心数据如何用,源组件不用关心数据来自哪里。 与组件一样,withSubscription和包装组件之间的契约完全是基于props的。这使得将一个HOC替换为另一个HOC变得很容易,只要它们为包装的组件提供相同的props。例如,如果您更改了数据获取库,这可能会很有用。
不要改变原始组件,使用组合
抵制在HOC中修改组件原型。
function logProps(InputComponent) {
InputComponent.prototype.componentDidUpdate = function(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
};
// The fact that we're returning the original input is a hint that it has
// been mutated.
return InputComponent;
}
// EnhancedComponent will log whenever props are received
const EnhancedComponent = logProps(InputComponent);
这里有一些问题。其一是输入组件不能与增强组件分开重用。更关键的是,如果你将另一个HOC应用到EnhancedComponent,它也会改变componentdiduupdate,那么第一个HOC的功能将被覆盖!这个HOC也不能用于没有生命周期方法的功能组件。
突变的hoc是一种有泄漏的抽象——消费者必须知道它们是如何实现的,以避免与其他hoc发生冲突。
hoc应该使用组合,而不是突变,将输入组件包装在容器组件中:
function logProps(WrappedComponent) {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log('Current props: ', this.props);
console.log('Previous props: ', prevProps);
}
render() {
// Wraps the input component in a container, without mutating it. Good!
return <WrappedComponent {...this.props} />;
}
}
}
参考文献: