使用 react 很久了,也使用 react 高阶组件很多次了,但是总也没有特别清楚的去总结一下高阶组件,现在终于有时间总结如下:
是什么
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
对比组件将props属性转变成UI,高阶组件则是将一个组件转换成另一个新组件。
例如 Redux
的 connect
方法,就是典型的高阶组件。
做什么
react 官方文档上说是解决交叉问题的。
直白了说就是解决了同一个数据在不同组件中渲染的问题
详细了说:
1 | class CommentList extends React.Component { |
1 | class BlogPost extends React.Component { |
以上两个组件 CommentList
和 BlogPost
都调用了 DataSource
的不同方法获取数据,渲染出不同的结果。但是整体相同的路由有:
- 挂载组件时, 向 DataSource 添加一个监听函数。
- 在监听函数内, 每当数据源发生变化,都是调用 setState函数设置新数据。
- 卸载组件时, 移除监听函数。
高阶组件的精华就是:
在一个大型应用中,在 DataSource
中获取数据,通过 setState
模式修改的情况会发生好多次,这个时候我们就可以抽象出一个模式,该模式允许我们在同一个地方写一个逻辑,在多个组件中都能使用。
所以我们是使用高级组件解决以上例子为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// 函数接受一个组件参数……
function withSubscription(WrappedComponent, selectData) {
// 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() {
// ……使用最新的数据渲染组件
// 注意此处将已有的props属性传递给原组件
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
1 | const CommentListWithSubscription = withSubscription( |
注意:高阶组件既不会修改原组件,也不会使用继承复制原组件的行为。相反,高阶组件是通过将原组件包裹(wrapping)在容器组件(container component)里面的方式来 组合(composes) 使用原组件。高阶组件就是一个没有副作用的纯函数。
包裹组件不关心数据是如何被使用的,你可以在 withSubscription
中添加任何参数进行更多的配置,包裹组件和 withSubscription
之间的传递也完全是通过 props
传递的。
注意点
不要在高阶组件内部修改(或以其它方式修改)原组件的原型属性。
如下的错误事例:1
2
3
4
5
6
7
8
9
10
11
12function logProps(InputComponent) {
InputComponent.prototype.componentWillReceiveProps(nextProps) {
console.log('Current props: ', this.props);
console.log('Next props: ', nextProps);
}
// 我们返回的原始组件实际上已经
// 被修改了。
return InputComponent;
}
// EnhancedComponent会记录下所有的props属性
const EnhancedComponent = logProps(InputComponent);
问题:
- input组件不能够脱离增强型组件(enhanced component)被重用。(复用性底)
- 如果你用另一个高阶组件来转变 EnhancedComponent ,同样的也去改变 componentWillReceiveProps 函数时,第一个高阶组件(即EnhancedComponent)转换的功能就会被覆盖。
正确的写法:1
2
3
4
5
6
7
8
9
10
11
12function 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} />;
}
}
}
优点:
- 复用性高,可以重复使用。
综上总结:高阶组件就是容器组件的一部分,也可以认为高阶组件就是参数化的容器组件定义
约定
1. 不传不相关的 props
高阶组件应该传递与它要实现的功能点无关的props属性。
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17render() {
// 过滤掉与高阶函数功能相关的props属性,
// 不再传递
const { extraProp, ...passThroughProps } = this.props;
// 向包裹组件注入props属性,一般都是高阶组件的state状态
// 或实例方法
const injectedProp = someStateOrInstanceMethod;
// 向包裹组件传递props属性
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
原因:确保高阶组件最大程度的 灵活性 和 可重用性。
2. 最大化使用组合
并不是所有的高阶组件看起来都是一样的。有时,它们仅仅接收一个参数,即包裹组件:
1 | const NavbarWithRouter = withRouter(Navbar); |
一般而言,高阶组件会接收额外的参数。例如 Relay 的一个例子:
1 | const CommentWithRelay = Relay.createContainer(Comment, config); |
我们常用的 redux
的 connect
就是一个很典型的例子1
2// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);
拨开之后:1
2
3
4
5// connect是一个返回函数的函数(译者注:就是个高阶函数)
const enhance = connect(commentListSelector, commentListActions);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(CommentList);
拨开之后是不是瞬间清晰了很多,说到底 connect
就是一个返回了高阶组件的函数
这种形式有点让人迷惑,有点多余,但是它有一个有用的属性。那就是,类似 connect 函数返回的单参数的高阶组件有着这样的签名格式, Component => Component
.输入和输出类型相同的函数是很容易组合在一起。
1 | // 不要这样做…… |
同样做法发还有 lodash
和 Ramda
他们均有 compose
这种组合函数。
3. 包装显示名字以便于调试
我们一般给高阶组件起名字都是 with*
,例如:高阶组件名字withSubscription
,包裹组件名字 CommentList
,使用时为 WithSubscription(CommentList)
原因:区分高阶组件和普通组件
注意事项
1. 不要再render函数中使用高阶组件
1 | render() { |
如上使用的话问题:
- 性能问题
- 重新加载一个组件会引起原有组件的所有状态和子组件丢失。
如果需要动态调用高阶组件,那么可以在组件的构造函数或生命周期函数中调用。
2. 必须将静态方法做拷贝
当使用高阶组件时,原始组件呗容器组件包裹之后就会失去原始组件原来的方法。解决这个问题就是需要我们把静态方法全部拷贝。
方法:
1 | import hoistNonReactStatic from 'hoist-non-react-statics'; |
- 分别导出组件自身的静态方法
1 | // 替代…… |
3. Refs属性不能传递
高阶组件可以传递所有的props属性给包裹的组件,但是不能传递refs引用。
refs是一个伪属性,React对它进行了特殊处理。如果你向一个由高阶组件创建的组件的元素添加ref应用,那么ref指向的是最外层容器组件实例的,而不是包裹组件。