第一章:React从入门到进阶之初识React
第一章:React从入门到进阶之JSX简介
第三章:React从入门到进阶之元素渲染
第四章:React从入门到进阶之JSX虚拟DOM渲染为真实DOM的原理和步骤
第五章:React从入门到进阶之组件化开发及Props属性传值
第六章:React从入门到进阶之state及组件的生命周期
第七章:React从入门到进阶之React事件处理
第八章:React从入门到进阶之React条件渲染
第九章:React从入门到进阶之React中的列表与key
第十章:React从入门到进阶之表单及受控组件和非受控组件
第十一章:React从入门到进阶之组件的状态提升
第十二章:React从入门到进阶之组件的组合使用
第十三章:React从入门到进阶之组件的组件的懒加载及上下文Context
第十四章:React从入门到进阶之Refs&DOM以及Refs转发
第十五章:React从入门到进阶之高阶组件
- 高阶组件(Higher Order Component,简称:HOC)是React中用于复用组件逻辑的一种高级技巧。HOC自身并不是React API的一部分,它是一种基于React的组合特性而形成的设计模式
- 简单而言:高阶组件就是一个把组件作为参数,并且返回一个新组件的函数。例如:
//WrappedComponent:参数是一个组件
function higherOrderComponent(WrappedComponent){
//返回一个新的组件
return class extends React.Component{
render(){
return <WrappedComponent .../>
}
}
}
- React 中组件是将props转换为UI,而高阶组件则是将组件转换为一个新的组件。高阶组件在第三方库中很常见,比如Redux中的connect和Relay中的createFragmentContainer
- 接下来我们来看一下,为什么要使用高阶组件以及如何使用高阶组件
我们先来看一个案例,假如:现在有一个博客系统,其中有一个博客帖子组件BlogPost和对应博客的评论组件,这两个组件都是通过订阅外部数据源来渲染数据。
//博客帖子组件
class BlogPost extends React.Component {
constructor(props){
super(props);
this.state = {
// 假设 “DataSource”是一个全局数据源变量
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} />;
}
}
//评论列表组件
class CommentList extends React.Component{
constructor(props){
super(props);
this.state = {
comments: DataSource.getComments()
}
}
componentDidMount(){
DataSource.addChangeListener(this.handleChange);
}
componentWillMount(){
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上调用的方法不同,和渲染的结果不同外,其它大部分代码基本上都是相同的:
- 在挂载时,向DataSource添加一个更改数据的侦听器,用来侦听数据源的变化从而更新组件状态
- 在侦听器的内部,当数据源发生变化时,调用setState来更新组件状态
- 在组件即将卸载前,移除侦听器
现在看来只有两个组件,那么我们试想一下,如果在一个大型项目中,肯定会有很多类似这种订阅DataSource然后调用setState的模式,如果我们每个组件都这么写一次,显然会造成代码的冗余,所以我们需要进行一下抽象,把这些相同或相似的逻辑定义在某个地方,并在需要用到这些相同或相似逻辑的组件中使用它,这正是高阶组件擅长的地方,也正是我们为什么要使用高阶组件的原因所在。
前面我们已经通过一个案例来讲解了我们为什么要使用高阶组件,接下来还是以上面的案例为例,来讲一下如何使用高阶组件。
在文章的开始,我们已经说过:高阶组件其实就是一个函数,它接收一个组件作为参数并且返回一个新的组件。
下面我们把这个案例进行一下抽象和重构
- 首先我们将两个组件相同或相似的逻辑进行一下抽象,也就是定义一个高阶组件。
- 该函数接收第一个参数是被包装的组件,第二个参数是一个函数,可以通过DataSource和当前的props返回我们需要的数据。
- 在该逻辑中我们将会把获取到的数据以prop的形式传递给传进来的组件
//此函数接收一个组件
function withSubscription(WrappedComponent, selectData){
//返回一个新的组件
return class extends React.Component{
constructor(props){
super(props);
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(){
//这里使用作为参数传进来的组件
//除了传递data属性外,还可能传递其它属性
return <WrappedComponent data={this.state.data} {...this.props} />
}
}
}
//原来的博客帖子和评论列表组件改造
function BlogPost(props){
//直接return渲染结果
return <TextBlock text={props.data} />;
}
function CommentList(props){
return (
<div>
{props.data.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
)
}
//使用高阶组件
const CommentListWithSubscription = withSubscription(CommentList,(DataSource)=>{DataSource.getComments()});
const BlogPostWithSubscription = withSubscription(BlogPost, (DataSource, props)=>{DataSource.getBlogPost(props.id)})
以上我们就实现了一个简单的高阶组件的定义和使用,这里需要注意的是:
- 高阶组件(HOC)不会修改传入的组件,也就是说作为参数传进来的组件直接使用即可,不要试图去修改它
- 也不要通过继承的方式来复制组件的行为
- 我们要做的就是通过将传进来的组件包装在容器组件中来组成一个新的组件,并返回。
- 高级组件(HOC)是一个纯函数,并且没有副作用
- 被包装的组件接收来自容器组件的所有props,同时也接收一个新的用于render的data prop
- HOC不需要关系数据的使用方式,而被包装的组件也不需要关心数据是怎么来的
- 因为withSubscription是一个普通函数,所以我们可以根据业务需要来对参数进行增添或者删除。
- 与组件一样,withSubscription和包装组件之间的契约完全基于之间传递的props。这种依赖方式使得替换 HOC 变得容易,只要它们为包装的组件提供相同的 prop 即可
使用高阶组件(HOC)需要遵守如下三大约定
- 将与HOC不相关的props传递给被包裹的组件
- 最大化可组合性
- 包装显示名称以便轻松调试
HOC 为组件添加新特性,自身不应该大幅度改变约定。HOC返回的组件与原组件应该保持类似的接口
HOC应该把与自己无关的props透传给被包裹的组件,大多数的HOC都应该包含一个类似下面的render方法:
render(){
//过滤出所有与此HOC相关的props,并且不进行透传(就是不会传给WrappedComponent)
const {extraProps,...passThroughProps} = this.props;
return(
<WrappedComponent {...passThroughProps} />
)
}
并不是所有的HOC都一样,有时候它们仅接收一个参数:就是被包裹的组件
有时候也会接收多个参数,比如在我们上面的案例中的withSubscription就接收了2个参数
在我们日常开发中有一种最常见的HOC就是React Redux中的connect
const NavbarWithRouter = withRouter(NavBar);//仅接收一个参数:被包裹的组件
const ConnectedComment = connect(selector, action)(CommentList);
上面的代码看上去有点复杂,但是仔细分析,不难发现,其实:connect就是一个函数,它的返回值也是一个函数,而这个函数的返回值又是一个高阶组件。
说白了,connect就是一个返回高阶组件的高阶函数,下面我们来拆分一下看看:
// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);
HOC 创建的容器组件会与任何其他组件一样,会显示在 React Developer Tools 中。为了方便调试,请选择一个显示名称,以表明它是 HOC 的产物。
最常见的方式是用 HOC 包住被包装组件的显示名称。比如高阶组件名为 withSubscription,并且被包装组件的显示名称为 CommentList,显示名称应该为 WithSubscription(CommentList):
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
好了,关于React的高阶组件就讲这么多。
下一章节中我们将会继续学习React中的 Render Props