高阶组件(HOC)是react中的高级技术,用来重用组件逻辑。但高阶组件自己并非React API。它只是一种模式,这种模式是由react自身的组合性质必然产生的。javascript
具体而言,高阶组件就是一个函数,且该函数接受一个组件做为参数,并返回一个新的组件。前端
const EnhancedComponent = higherOrderComponent(WrappedComponent);
复制代码
一般咱们写的都是对比组件,那什么是对比组件呢?对比组件将 props
属性转变成 UI,高阶组件则是将一个组件转换成另外一个组件。java
高阶组件在 React 第三方库中很常见,好比 Redux 的 connect
方法和 Relay 的 createContainer
。react
以前用混入(mixins)技术来解决横切关注点。但是混入(mixins)技术产生的问题要比带来的价值大。因此就移除混入(mixins)技术,对于如何转换你已经使用了混入(mixins)技术的组件,可查看更多资料。显然,横切关注点就用高阶组件(HOC)来解决了。git
1.假设有一个评论组件(CommentList),该组件从外部数据源订阅数据并渲染github
// CommentList.js
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> ); } } 复制代码
2.而后,有一个订阅单个博客文章的组件(BlogPost)算法
// BlogPost.js
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
}
}
conponentDidMount() {
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} />; } } 复制代码
3.以上,评论组件 CommentList
和 文章订阅组件 BlogPost
有如下不一样数组
DataSource
的方法不一样但是,它们也有相同点app
DataSource
添加一个改变的监听器;setState
;一个大型应用中,从 DataSource
订阅数据并调用 setState
的模式会屡次使用,这个时候做为前端就会嗅出代码要整理一下,可以抽出相同的地方做为一个抽象,而后许多组件可共享它,这就是高阶组件产生的背景。函数
4.咱们使用个函数 withSubscription
让它完成如下功能
const CommentListWithSubscription = withSubscription(
CommentList,
DataSource => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
复制代码
上面函数 withSubscription
的第一个参数是咱们以前写的两个组件,第二个参数检索所须要的数据(DataSource 和 props)。
那这个函数 withSubscription
该怎么写呢?
const withSubscription = (TargetComponent, 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);
}
componentDidMount(){
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render(){
return <TargetComponent data={this.state.data} {...this.props} /> } } } 复制代码
5.总结下
props
属性以及新的数据 data
用于渲染输出。高阶组件并不关心数据的使用方式,被包裹组件不关心数据来源。props
属性。这就是能够替换另外一个高阶组件,只要他们提供相同的 props
属性给被包裹组件便可。你能够把高阶组件当成一套主题皮肤。如今,咱们对高阶组件已经有了初步认识,但是实际业务当中,咱们写高阶组件时,容易写着写着就修改了组件的内容,千万要抵住诱惑。好比
const logProps = (WrappedComponent) => {
WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
console.log('CurrentProps', this.props);
console.log('NextProps', nextProps);
}
return WrappedComponent;
}
const EnhancedComponent = logProps(WrappedComponent);
复制代码
logProps
有几个问题WrappedComponent
不能独立于加强型组件(enhanced component)被重用。EnhancedComponent
上应用另外一个高阶组件 logProps2
,一样也会改去改变 componentWillReceiveProps
,高阶组件 logProps
的功能就会被覆盖。const 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} />; } } } 复制代码
不知道你发现没有,高阶组件和容器组件模式有相同之处。
props
属性给子组件;高阶组件返回的那个组件与被包裹的组件具备相似的接口。
render(){
// 过滤掉专用于这个高阶组件的 props 属性,丢弃 extraProps
const { extraProps, ...restProps } = this.props;
// 向被包裹的组件注入 injectedProps 属性,这些通常都是状态值或实例方法
const injectedProps = {
// someStateOrInstanceMethod
};
return (
<WrappedComponent injectedProps={injectedProps} {...restProps} /> ) } 复制代码
约定帮助确保高阶组件最大程度的灵活性和可重用性。
并非全部的高阶组件看起来都是同样的。有时,它们仅接收单独一个参数,即被包裹的组件:
const NavbarWithRouter = withRouter(Navbar);
复制代码
通常而言,高阶组件会接收额外的参数。在下面这个来自 Relay 的示例中,一个 config
对象用于指定组件的数据依赖:
const CommentWithRelay = Relay.createContainer(Comment, config);
复制代码
高阶组件最多见签名以下所示:
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
复制代码
能够这么理解
// connect 返回一个函数(高阶组件)
const enhanced = connect(commentSelector, commentActions);
const ConnectedComment = enhanced(Comment);
复制代码
换句话说,connect
是一个返回高阶组件的高阶函数!可是这种形式多少让人有点迷惑,可是它有一个性质,只有一个参数的高阶函数(connect
函数返回的),返回是 Component => Component
,这样就可让输入和输出类型相同的函数组合在一块儿,在一块儿,在一块儿
// 反模式
const EnhancedComponent = withRouter(connect(commentSelector, commentActions)(Comment));
// 正确模式
// 你可使用一个函数组合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同样的
const enhanced = compose(
withRouter,
connect(commentSelector, commentActions)
);
const EnhancedComponent = enhanced(Comment);
复制代码
包括 lodash(好比说 lodash.flowRight
), Redux 和 Ramda 在内的许多第三方库都提供了相似 compose
功能的函数。
若是你的高阶组件名字是 withSubscription
,且被包裹的组件的显示名字是 CommentList
,那么就是用 WithSubscription(CommentList)
这样的显示名字:
const withSubscription = (WrappedComponent) => {
// return class extends React.Component { /* ... */ };
class WithSubscription extends React.Component { /* ... */ };
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
return WithSubscription;
}
const getDisplayName = (WrappedComponent) => {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
复制代码
**React的差分算法(称为协调)**使用组件标识肯定是否更新现有的子树或扔掉它并从新挂载一个新的。若是 render
方法返回的组件和前一次渲染返回的组件是彻底相同的(===),React就递归地更新子树,这是经过差分它和新的那个完成。若是它们不相等,前一个子树被彻底卸载掉。
通常而言,你不须要考虑差分算法的原理。可是它和高阶函数有关。由于它意味着你不能在组件的 render
方法以内应用高阶函数到组件:
render() {
// 每一次渲染,都会建立一个新的EnhancedComponent版本
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 就会引发每一次都会使子对象树彻底被卸载/从新加载
return <EnhancedComponent />; } 复制代码
上面代码会致使的问题
问题:当你应用一个高阶组件到一个组件时,尽管,原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法。
// 定义静态方法
WrappedComponent.staticMethod = function() {/*...*/}
// 使用高阶组件
const EnhancedComponent = enhance(WrappedComponent);
// 加强型组件没有静态方法
typeof EnhancedComponent.staticMethod === 'undefined' // true
复制代码
解决方案: (1)能够将原始组件的方法拷贝给容器
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须得知道要拷贝的方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
复制代码
(2)这样作,就须要你清楚的知道都有哪些静态方法须要拷贝。你可使用 hoist-non-react-statics 来帮你自动处理,它会自动拷贝全部非React的静态方法:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
复制代码
(3)另一个可能的解决方案就是分别导出组件自身的静态方法。
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...export the method separately...
export { someFunction };
// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
复制代码
refs
属性不能贯穿传递通常来讲,高阶组件能够传递全部的 props
属性给包裹的组件,可是不能传递 refs
引用。由于并非像 key
同样,refs
是一个伪属性,React 对它进行了特殊处理。若是你向一个由高阶组件建立的组件的元素添加 ref
应用,那么 ref
指向的是最外层容器组件实例的,而不是被包裹的组件。
React16版本提供了 React.forwardRef
的 API 来解决这一问题,可在 refs
传递章节中了解下。