概念
- Higher-Order Function,是函数式编程里的一个基本概念,接受函数作为输出,或者输出一个函数。
- 函数式编程推崇每个函数只执行一个功能,整体通过函数组合起来,Ramda.js从右向左的思想很地道。
- reduce、map等,都是一种高阶函数
- React里的高阶组件Higher-Order Components也是如此,它接受一个React组件作为输入,输出一个新的React组件。如同用一个容器进行包裹,返回一个增强的组件,使得代码更加有抽象性,具有复用性,可以对props、state进行控制,也可以对render进行劫持
- 正如组件把props -> ui,高阶组件做的是把组件 -> 另一个组件。看起来就像是这样
const EnhancedComponent = higherOrderComponent(WrappedComponent);
- 高阶组件应该是纯函数,是没有side-effects的。HOC本身不关心数据会怎么被使用,而被包裹的组件不关心数据从何而来。高阶组件和被包裹的组件之间,就是完全
props-based
。可以把HOC想像成带参数的容器组件(容器组件管理订阅和状态,并且以props的形式传入UI组件)。 Redux的
connect
和Relay
里的createFragmentConttainer
也都是高阶函数1
2
3
4
5
6
7
8// React Redux's connect
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
// 其实大概是这么一个机制
// connect is a function that returns another function
const enhance = connect(commentListSelector, commentListActions);
// The returned function is a HOC, which returns a component that is connected to the Redux store
const ConnectedComment = enhance(CommentList);
使用原则
不要修改被包裹组件的prototpye
- 通过WrappedComponent.prototype,是可以访问到如
componentWillReceiveProps
的。 - 但是不应该这么做,如果希望在生命周期钩子上做一些事,应该在HOC自己的对应的生命周期上去做,能达到同样的效果
- 不修改WrappedComponent.prototype,才不至于不恰当地影响它;而且这样即使多次套HOC,也不会有冲突;也符合分离的原则,不至于写出有毒的代码。
经过HOC包装后,返回的组件应该和WrappedComponent有相似的接口
- 这么做的目的是提升组件们的可复用性
额外的props应该被分离传入,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17render() {
// Filter out extra props that are specific to this HOC and shouldn't be
// passed through
const { extraProp, ...passThroughProps } = this.props;
// Inject props into the wrapped component. These are usually state values or
// instance methods.
const injectedProp = someStateOrInstanceMethod;
// Pass props to wrapped component
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
设置合适的displayName方便debug
- displayName就是用于debugging message的,一般来说是不需要特别设置的,因为他会根据组件的类名有一个默认值
- 但是如果使用了高阶组件,合理的设置displayName,就能让调试过程更加简单和顺利(目的是能清楚的区分组件是高阶组件还是一般的组件)。
官方文档推荐的做法是:
1
2
3
4
5
6
7
8
9function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
应用场景
属性代理:通过被包裹的组件来操作Props
- 通过高阶函数的装饰,生命周期是:didmount -> HOC didmount -> (HOCs didmount) -> … -> (HOCs willunmount)-> HOC willunmount -> unmount
既然是包裹,很容易想到,可以随意增加外层,来添加样式、方便布局等,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import React, { Component } from 'react'
const ExampleHoc = WrappedComponent => {
return class extends Component {
render() {
return (
<div style={{display: 'flex'}}>
<WrappedComponent {...this.props} />
</div>
)
}
}
}
export default ExampleHoc可以读取、编辑、增加、移除从WrappedComponent传来的props,需要注意,应该对高阶函数的props作新的命名,防止混淆。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13import React, { Component } from 'react'
const ExampleHoc = WrappedComponent => {
return class extends Component {
render() {
const newProps = {
name: newText,
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
export default ExampleHoc可以通过WrappedComponent提供props和回调函数抽象state,比如把原组件抽象成展示型组件,分离内部状态,弄成无状态组件
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
27import React, { Component } from 'react';
const ExampleHoc = WrappedComponent => {
return class extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
}
}
onNameChange = e => {
this.setState({
name: e.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange,
}
}
return <WrappedComponent {...this.props} {...newProps} />
}
}
}
export default ExampleHoc
反向继承:高阶组件继承于被包裹的组件
- 原理就是让高阶函数返回的组件继承于被包裹的组件WrappedComponent,和属性代理不同,方法可以通过super来顺序调用,此时的生命周期是:didmount -> HOC didmount ->(HOCs didmount) -> willunmount -> HOC willunmount ->(HOCs willunmount)
- 这种结构,使得高阶函数可以使用WrappedComponent的state、props、生命周期和render方法!
渲染劫持,高阶组件控制WrappedComponent的渲染过程,但是如果元素树里有函数类型的React组件,就不能操作组件的子组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const ExampleHoc = WrappedComponent => {
return class extends WrappedComponent {
render() {
const eleTree = super.render()
let newProps = {}
if(eleTree && eleTree.type === 'input') {
newProps = {value: '这不能被渲染'}
}
const props = Object.assgin({}, eleTree.props, newProps)
const newEleTree = React.cloneElement(eleTree, props, eleTree.props.children)
return newEleTree
}
}
}虽然也可以读取、修改、删除WrappedComponent的state,但是应该尽量减少,因为会增加混乱,或者至少要重新命名state
高阶函数接受其他参数
代码上增加一层即可
1
2
3
4
5
6
7
8
9
10import React, { Component } from 'react'
const ExampleHoc = (key) => (WrappedComponent) => {
return class extends Component {
render() {
return <WrappedComponent {...this.props} />
}
}
}又比如,直接接受复数的参数,第二个参数可以传入一个函数,其返回值就是关心的数据,由此我们可以随意传入更多的参数
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// 使用
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
// 定义
// 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} />;
}
};
}