解读React高阶组件
当开发的React组件数量达到一定程度时,就必定会遇到一个问题,某些组件中有部分逻辑是完全相同的,但这个逻辑又不影响视图,所以不太适合抽出新的组件。为了重用这部分逻辑,就有了高阶组件。
定义
简单来说高阶组件就是一种装饰器模式,用于在现有组件上增强功能。这种模式通过一个函数实现,该函数称作高阶组件工厂函数,接受一个组件作为输入然后返回新的组件,这个新的组件就是拥有新功能的高阶组件了。
总的来说,高阶组件的目的分为如下两点:
- 重用逻辑代码
- 无侵入的增强现有组件行为
实现
根据传入组件和返回组件的关系,实现方式又可分为两类,下面分别介绍。
属性代理
顾名思义,就是在传入参数到达原组件之前,对属性做一层代理做增强处理,除了传入属性有变化外,高阶组件不会侵入原组件的渲染逻辑。最简单的实例如下:
function PropsProxy(WrappedComponent) {
return function newRender(props) {
return <WrappedComponent {...props} /> // 这里可以添加增强props的逻辑
};
}
虽然看起来比较简单,但作用却不小。主要的应用场景如下。
操作props
以上的实例我们也看到了虽然不侵入原组件的渲染,但可以决定传入哪些参数。除了原封不动的传入外,当然也可以增删改了。示例如下:
function addNewProps(WrappedComponent, newProps) {
return class NewComponent extends React.Component {
render() {
return <WrappedComponent {...this.props} {...newProps} />
}
}
}
需要注意当删除和修改props时,一定要确保原组件能够正确渲染。
访问ref
该类高阶组件可以获取到原组件的渲染实例。首先要知道这种使用方式是不被推荐的,因为虽然获取到组件引用后可以做很多事,但正因为如此,给予太多的控制权反而不容易管理并且相当容易出现意外问题。这里只是展现一下可能性,不到万不得已不要采用这种方式。用法如下:
function getRefs(WrappedComponent) {
return class NewComponent extends React.Component {
constructor() {
super(...arguments);
this.linkRef = this.linkRef.bind(this);
}
linkRef(ref) {
this._root = ref;
}
render() {
const props = {...this.props, ref: this.linkRef};
return <WrappedComponent {...props} />
}
}
}
抽取state
实际上这个应用之前就已经见到过了,就是react-redux的connect部分。通过map函数抽取redux的store需要的state部分,然后在生命周期钩子函数中监听store变化触发高阶组件的更新。简单实现如下:
function connect(mapStateToProps, mapDispatchToProps) {
return function(WrappedComponent) {
class NewComponent extends React.Component {
constructor() {
super(...arguments);
this.onChange = this.onChange.bind(this);
this.store = {};
}
componentDidMount() {
this.context.store.subscribe(this.onChange);
}
componentWillUnmount() {
this.context.store.unsubscribe(this.onChange);
}
onChange() {
this.setState({}); // 触发更新,无具体逻辑
}
render() {
const store = this.context.store;
const newProps = {
...this.props,
...mapStateToProps(store.getState()),
...mapDispatchToProps(store.dispatch)
};
return <WrappedComponent {...newProps} />
}
};
NewComponent.contextTypes = {
store: React.PropTypes.object;
};
return NewComponent;
};
}
包装组件
以上行为实际上都是在props上做增强,实际上我们完全可以在不改变原组件的渲染逻辑下包装一层渲染,例如增强样式。
function addStyle(WrappedComponent, style) {
return class NewComponent extends React.Component {
render() {
return (
<div style={style}>
<WrappedComponent {...this.props} />
</div>
);
}
}
}
反向继承
这种实现和属性代理最大的区别在于它会侵入性的改变原组件的渲染逻辑。属性代理下原组件仍然经历了原始的生命周期和渲染逻辑,高阶组件和原组件是属于父子关系的组件,并不会影响到原组件内部。而反向继承下高阶组件直接继承了原组件,意味着可以通过this轻易修改组件内部的生命周期和渲染逻辑。简单的实例如下:
function Inheritance(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
渲染劫持
大部分情况下,属性代理的高阶组件是优于反向继承的,因为组合相比于继承更加低耦合,易拓展。但只有反向继承能做到的就是能更细粒度的控制原组件的渲染过程与结果,因为通过super.render我们可以直接获取到渲染元素树并在其基础上操作,也就是渲染劫持了。
但有一点需要注意,通过super.render获取的元素树并不是被完整解析的,因为React元素分为两类:
- DOM节点,由字符串表示
- 组件,由render函数表示
虽然最终都会被完整解析为DOM,但至少在渲染这一步,组件形式的React元素仍是未被解析的,也就意味着我们不能通过渲染劫持的形式操作高阶组件的子组件。
缺点
以上就是高阶函数的大部分应用场景,在拓展组件多样性上当然是很强大,但也存在着如下的缺点需要注意:
- 命名丢失,由于每个高阶组件都是一个新的组件,所以会丢失原组价的显示名不方便调试,需要手动给组件赋予displayName属性名。
- 当多个高阶组件嵌套时,无法确定子组件props的来源。
- 若props重复时,可能会相互覆盖并且不会有报错。
- 加深了组件层级,不便于维护和调试。
另一种选择——Render Props
高阶组件的主要目的还是为了提高代码重用率,而这并不是唯一的方法,Render Props就是另外一种解决方案。其核心思想在于将组件的state作为props传递给函数子组件渲染。该模式下实现代码重用的就不是一个高阶函数构造工厂了,而是一个React组件,这个组件必须有一个函数子组件,该组件的render函数会通过this.props.children直接调用函数子组件并将需要的数据传入,得到的结果就可以作为React组件render中变化的一部分,重用的部分就封装到 React组件中。举个实例如下:
比如针对不同的modal,控制显隐的逻辑是可以重用的,我们抽出来作为ModalContainer组件
class ModalContainer extends React.Component {
state = {
visible: false
};
show() {
this.setState({visible: true});
}
hide() {
this.setState({visible: false})''
}
render() {
const { visible } = this.state;
const { children } = this.props;
return children({
visible,
show: this.show,
hide: this.hide
});
}
}
上面的children就是不同modal变化的函数组件了,传入的参数则为通用逻辑。实例如下:
class Modal extends React.Component {
render() {
return (
<ModalContainer>
{
modal => (
<div>
<AModal visible={modal.visible} handleHide={modal.hide}></AModal>
<Button onClick={modal.show}>Click</Button>
</div>
)
}
</ModalContainer>
);
}
}
可以看到由于函数子组件包含很多可能性,使得该方法相当灵活,也规避了高阶组件的缺点。当然该方法也不是万能的,依然存在一些问题:
- 由于函数子组件没有生命周期,则每次外层组件渲染时都会调用该函数,无法避免渲染浪费。
- 渲染粒度增大,相比于高阶组件的装饰者模式要增加不少代码量。
小结
总之这两种代码重用方式各有利弊,render props相比于高阶组件的形式更加灵活,但更加难以复用包装组件并且会有潜在的性能问题。实际项目中应该根据具体情况选择使用。
-- EOF --