其实之所以讲到这里是因为,当我们使用React的组件化开发Web应用的时候,就会遇到这样的问题,很多组件需要某个功能,但是对应的功能与界面并没有关系,无法直接简单的抽取成为一个组件,但是如果说将类似的功能在不同的组件当中实现的话,就违背了所谓的Don't Your Repeat(DRY原则)
因此我们就需要使用到本节中讲到的内容 React高级组件HOC(High Order Component):
其中对应的内容如下:
- 高级组件的概念及其应用
- 以函数为子组件的模式
最终的目的就是最大程度的代码之间的复用,对应的两者的策略不同,我们也应该针对特定的应用场景进行选择!
01|什么是HOC(High Order Component)?
简单来讲HOC并不是React提供的某种特定的API,而是一种模式,增强现有组件的功能,对组件进行功能拓展!
- 高阶组件接受一个函数作为输入,返回一个新的组件作为结果 对应的结果对原有的输入进行了功能上的增强!
可能我这么说的话,时机上来看还是不是特别的直观!我们使用代码来进行简单的演示:
import React,{Component} from "react";
const removeUserProp = WrappedComponent=>{
return class WrappingComponent extends Component{
render(){
const {user,...otherProps} = this.props;
return (
<WrappedComponent {...anotherProp} />
);
}
}
}
其中我们使用函数表达式的方式写了一个高阶函数:
- 接受一个现有的组件
WrappedComponent
作为输入参数 - 返回一个组件类
WrappingComponent
,对应的render的结果就是 剥离了user信息的被包装的组件! - 其中所谓的增强其实就是 屏蔽了不需要的属性字段
那么通过这个简单的代码,我们又能够想到什么?
- 如果说别的组件需要使用类似的通能的话或者同样的功能,就能够直接使用高阶组件进行
增强
了!
那么对应的根据新组件和传入组件参数的关系,高阶组件的实现方式可以分为两大类;
- 代理的方式实现高阶组件
- 继承的方式实现高阶组件
01|代理方式的高阶组件
第一个代码示例其实就是所谓的代理的方式的高阶组件,返回的组件直接继承React.Component类
- 新组件扮演的角色就是:传入参数的代理
- 新建的render函数中把被包裹的组件渲染出来
- 高阶组件做的工作通常是额外的功能增强,除此之外的工作都交给被包裹的组件
- 如果说,高阶组件所做的功能除了render之外的声明函数都不涉及的话,也不需要维护自身的状态的话,就可以以函数组件的方式返回!
const removeUserProp = WrappedComponent=>{
return function WrappingComponent(props){
const {user,...otherProps} = props;
return <WrappedComponent {...otherProps} />
}
}
这样依赖对应的逻辑更加的清晰,但是这种所谓的函数组件的功能是非常有限的,因此我们主要介绍class组件的方式:
代理方式的高阶组件可以应用在下列场景当中:
- 操作prop
- 访问ref
- 抽取状态
- 包装组件
- 操作prop
代理类型高阶组件返回的组件(增强的组件),渲染的过程是由新组件的render函数所控制的!
那么也就是说,被包裹的组件如何使用是由render所决定的! 可以看成是一个代理
render函数中,新组件的this.props包含所有新组建所接收到的属性,最简单的方式就将是接收到的所有属性{this.props}原封不动的传递给被包裹的组件 因为是做功能方面的增强,我们一般都是 增删改props的方式传递给被包裹的组件!
import React,{Component} from "react";
const appendProps = (WrappedComponent,newProps)=>{
return class WrappingComponent extends Component{
render(){
return <WrappedComponent {...this.props} {...newProps}/>
};
}
}
通过以上代码就很好的诠释了增强功能,并且方便给不同的组件添加不同的属性!
- 访问ref(reference)
访问ref并不是React所推荐的做法,但是是可以使用HOC高阶组件实现这种功能
import React,{Component} from "react";
const refHoc = WrappedComponent=>{
return class WrappingComponoent extends Component{
constructor(){
super(...arguments);
this.linkRef = this.linkRef.bind(this);
}
linkRef(wrappedInstance){
this._root = wrappedInstance;
}
render(){
const props = {...this.props,ref:this.linkRef};
return <WrappedComponent {...props} />
}
}
}
以上代码在linkRef被调用的时候,就得到了被包裹组件(WrappedComponent)的DOM实例,被保存在了新组件的_root属性中!
但是实际上用的还是比较少!
- 抽取状态
其实对应的我们在使用react的时候,react-redux中的connect函数执行完毕返回的函数是作为高阶组件的!
其中我们可以通过 傻瓜组件 和 容器组件 进行理解:
- 傻瓜组件:无状态的组件,负责视图的渲染
- 容器组件:负责将状态传递给傻瓜组件,不负责视图
傻瓜和容器就是妥妥的抽取状态,我们通过代码,简单理解一下connect高阶组件
import React,{Component} from "react";
const doNothing = ()=>({});
const connect = (mapStateToProps=doNothing,mapDispatchToProps=doNothing)=>{
return function(WrappedComponent){
class HocComponent extends Component{
//lifeCycle fucntion
}
HocComponent.contextTypes = {
store:React.PropTypes.object
}
return HocComponent;
}
}
和react-redux中的connect方法一样,我们定义的connect方法接收两个参数,分别是mapStateToProps和mapDispatchToProps.返回的组件类预期能够访问一个叫做store的context的值! 对应的context的值由Provider提供! 对应的我们通过this.context.store
进行访问Provider提供的store实例,对应的实现类似的功能的话,HocComponent组件需要一系列的成员函数来维持内部状态和store同步,对应的代码如下所示:
import React,{Component} from "react";
import {PropTypes} from "prop-types";
class HocComponent extends Component{
constructor(){
super(...arguments);
this.onChange = this.onChange.bind(this);
this.stroe = {};
}
componentDidMount(){
this.context.store.subscribe(this.onChange);
}
componentWillUnmount(){
this.context.store.unsubscribe(this.onChange);
}
onChange(){
this.setState({});
}
}
通过借助store的subscribe和unsubscribe函数,HocComponent保证了每当Redux的Store上状态发生变化的时候,都会驱动组件的更新!
虽然谁应该返回一个有状态的组件,但是真正的组件是存在于Redux上的Store上面的,组件内的状态是什么其实对应的并不重要,使用组件状态唯一原因是通过this.setState对状态进行更新的过程!
对应的当Redux上面的Store发生了变化的时候,就可以通过this.setState重设组件状态,驱动组件更新的过程!
对应的HocComponent的render函数如下所示:
render(){
const store = this.context.store;
const newProps = {
...this.props,
...mapStateToProps(store.getState()),
...mapDispatchToProps(store.dispatch);
}
return <WrappedComponent {...newProps}/>;
}
render中的逻辑类似于"操纵Props"的方式,渲染工作完全交给了WrappedComponent,但是控制住了WrappedComponent的props,该组件能够渲染什么完全取决于props!
以上的代码,通过store.getState和store.dispatch可以传递给WrappedComponent组件对应的状态和dispatch方法!
最终通过调用connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)
里面传入的组件进行了功能增强!
- 继承方式的高阶组件
继承方式的高阶组件采用继承关系关联作为参数的组件和返回的组件!如果说传入的参数为WrappedComponent那么对应的返回的组件则直接继承WrappedComponent
对应的我们可以使用这种方式重新实现一遍之前介绍的高级组件的代码演示示例:
const removeUserProps = WrappedComponent=>{
return class NewComponent extends WrappedComponent{
render(){
const {user,...otherProps} = this.props;
this.props = otherProps;
return super.render();
}
}
}
对应的代理和继承最大的区别就在于,使用被包裹组件的方式
- 在代理方式下:
<WrappedComponent {...otherProps} />
- 在继承的方式下:
return super.render()
因为我们创造的组件继承WrappedComponent因此直接调用父类的渲染函数即可!
代理方式下产生的新组件和参数组件是两个不同的组件,一次渲染两个组件都需要经历各自的生命周期!
继承方式下两者合而为一!只有一个生命周期!
但是以上的代码修改this.props,对应的做法不太妥当,实际上这样处理的话不太合适,继承方式的高阶组件可以应用与下列场景:
- 操作Porps
- 操作生命周期函数
- 操作Props
集成方式的高阶组件对应的也能够操作Props,除了上面不安全的直接修改this.props的方法,还能够利用React.cloneElement
让组件重新绘制!
const modifyPropsHOC = WrappedComponent=>{
return class newComponent extends WrappedComponent{
render(){
const elements = super.render();
const newStyle = {
color:(elements && elements.type === div) ? "red" : "blue"
}
const newProps = {...this.props,style:newStyle};
return React.cloneElement(elements,newProps,elements.props.children);
}
}
}
React.cloneElement(
element,
[props],
[...children]
)
以 element
元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 key
和 ref
将被保留。
以上的代码添加了一个新的属性,样式,如果说元素存在并且对应的顶层元素类型为div的话,那么就设置color颜色为红色否则为蓝色!
最后将本身所存在的props和心得prop属性合并在一起! 最后通过React.cloneElement
让产生新组件重新渲染一遍!
- 操作生命周期函数
因为继承方式的高阶函数返回的新组件继承了参数组件,因此可以重新定义任何一个React组件的生命周期函数,对应的这是继承方式高阶函数的特用的场景,则代理的方式无法修改传入组件的生命周期函数
如果说参数组件只有在用户登录的时候才能够渲染对应的界面的时候,那么对应的代码则如下所示:
const OnlyForLoggedInHOC = WrappedComponent=>{
return class NewComponent extends WrappedComponent{
render(){
if(this.props.loggedIn){
return super.render();
}else{
return null;
}
}
}
}
其中还可以通过shouldComponentUpdate函数,只要prop中的useCache不为逻辑false就不做重新渲染
const cacheHOC = WrappedComponent=>{
return class newComponent extends WrappedComponent{
shouldComponentUpdate(nextProps,nextState){
return !nextProps.useCache;
}
}
}
其实由此便可以看出,代理和继承的两种方式,各方面看来代理还是要优于继承方式!
优先考虑组合,之后再考虑继承
03|高阶组件的显示名
其实使用了所谓的高阶组件都会产生一个新的组件,使用该组件就丢失掉了参数组件的 显示名,因此往往需要给告诫组件重新定义一个 显示名 不然的话,在我们debug或者说查看日志的时候组件名的显示就会非常的莫名其妙!
如何给组件添加显示名?
- 高阶组件类的displayName添加一个字符串类型的值
如果说我们需要在react-redux中的connect返回的作为高阶函数,高阶组件的名字包含Connect,同时包含参数组件WrappedComponent的名字,因此需要这么设置!
const getDisplayName = WrappedComponent=>{
return WrappedComponent.displayName || WrappedComponent.name || "Component";
}
HOCComponent.displayName = `Connect(${getDisplayName(WrappedComponent)})`;
04|曾经的Mixin
除了上面所说到的高阶组件,其实React中还有一种进行代码复用的方式,就叫做Mixin,但是我们并不推荐使用它!
const shouldUpdateMixin = {
shouldComponentUpdate(){
reurn !this.props.useCache;
}
}
但是它存在对应的局限性,Mixin只能够在用React.clearClass的语法创建的组建才能够使用,如果说ES6的class语法则不适用
const SampleComponent = React.createClass({
mixins:[shouldUpdateMixin],
render(){
//render view
}
})
使用React.createClass的方式创建的SampleComponent的组件,因为有Mixins字段,成员方法中就混入了shouldUpdateMixin这个对象里面的方法!
那么为什么不推荐使用Mixin呢?
- 过于灵活
- 作为设计原则,我们应该尽量将state从组件中抽离出来,mixin则鼓励在React组件中添加状态
- ES6中的Class语法不支持Mixin,并且同时被官方废弃了!
02|以函数作为子组件
高阶组件并不是作为提高React组件代码重用的唯一方法,高阶组件通过拓展原有组件的功能的主要方法是通过对Props的控制(增加/减少/修改Props)进行的!
代理方式的高阶组件,返回的组件和输入的组件说到底是两个组件(原有组件和功能"增强"组件)对应的父子关系,对应的组件通信方式是通过props来进行通信的!
但是对应的高阶组件也有缺点,就是对原组件的Props有了固化的要求,也就是说,能不能把一个高阶组件作用于某个组件X,先看一下这个组件X是不是能够接受高阶组件传过来的props,如果说组件X并不能够支持这些props的话,或者说对这些props的命名有所不同的话,是不能够引用这个高阶组件的!
假设有一个高阶组件addUserProp,读取对应的loggedinUser,把这个数据作为名为user的prop传递给参数组件
import React,{Component} from "react";
const addUserProp = WrappedComponent=>{
return class WrappingComponent extends Component{
render(){
const newProps = {user:loggedinUser};
return <WrappedComponent {...this.props} {...newProps} />;
}
}
}
像前文中所说的那样,那么作为参数的组件,需要能够接受名为user的prop,不然对应的高阶组件完全没效果!
但是如果说作为层层传递的props,这种高阶组件这种要求参数组件必须和自己有契约的方式,会带来很多麻烦!
因此为了更好地解决该问题,于是我们就使用 以函数为子组件的方式,其实就是为了客服高阶组件的这种局限而生的!实现代码重用的不是一个函数而是一个真正的React组件! 对应的约束其实也是这样的:
- 子组件必须是一个函数
- 在组件实例的生命周期中,this.props.children引用的就是自组件,render函数直接会把this.props.children当作函数来调用!
接下来我们按照上面的需求重新使用函数为子组件的方式重新实现一遍:
import React,{Component} from "react";
import {PropTypes} from "prop-types";
const loggedinUser = 'mock user';
class AddUserProp extends Component{
render(){
const user = loggedinUser;
return this.props.children(user);
}
}
AddUserProp.propTypes = {
children:PropTypes.func.isRequired
}
以上代码中与被增强代码的联系就是this.props.children
对应的children
为函数类型!在render函数中调用this.props.children
参数就是我们传递下去的user!
如果说想让一个组件把user显示出来,代码就是这样的:
<AddUserProp>
{user=><div>{user}</div>}
</AddUserProp>
如果说我们想将user作为prop传递给一个接受user名prop的组件Foo,那么只需要使用另外一个函数作为AddUserProp的子组件就行了:
<AddUserProp>
{user=><Foo user={user} />}
</AddUserProp>
03|一个倒计时的高阶组件
我们利用以函数为子组件的模式构建一个复杂一点的CountDown实现倒计时的通用功能:
- 初始化组件
import React,{Component} from "react";
class CountDown extends Component{
constructor(){
super(...arguments);
this.state = {count:this.props.startCount};
}
}
首先需要有一个开始的值,对应的倒计时组件的作用就是持续驱动子组件进行更新操作!
当对应的CountDown组件完成挂载之后我们就需要通过setInterval函数启动没秒倒计时更新内部的状态:
componentDidMount(){
this.intervalHandle = setInterval(()=>{
const newCount = this.state.count - 1;
if(newCount>=0){
this.setState({count:newCount});
}else{
window.clearInterval(this.intervalHandle);
}
},1000);
}
使用this.intervalHandle
作为间隔调用的标记,当对应的新的count值小于且不等于0的时候就清除该标记!
其中一定要在componentDidUnmount里面一定要取消并且清理掉intervalHandle,因为CountDown完全可能在没有倒计时为0的时候被卸载! 因此卸载之前一定需要清除interval的标记:
componentUnmount(){
if(this.intervalHandle){
window.clearInterval(this.intervalHandle);
}
}
对应的渲染部分,和之前讲到的同理:
render(){
reutrn this.props.children(this.state.count);
}
我们还需要对CountDown进行约束:
import PropTypes from "prop-types";
CountDown.propTypes = {
children:PorpTypes.func.isRequired,
startCount:PropTypes.number.isRequired
}
如果说有对应的组件需要倒计时的功能,就需要恰当的将函数作为CountDown的子组件即可:
<CountDown startCount={10}>
{
count=><div>{count}</div>
}
</CountDown>
如果说当对应的倒计时为0的时候,就显示 新年快乐! Happy New Year!
<CountDown startCount={10}>
{
count=> <div>{count > 0 ? count : "新年快乐!"}</div>
}
</CountDown>
04|性能优化问题
函数作为子组件的方式非常灵活,但是也有其对应的缺点,也就是说,函数作为子组件的方式难以优化!
- 外层组件的更新过程,都需要执行一个函数获得子组件的实际渲染效果!
- 每次渲染都需要执行函数,无法使用
shouldComponentUpdate
进行细粒度的控制,使用高阶组件可以直接使用该生命周期函数避免重新渲染! - 如果说定制外层组件的shouldComponentUpdate,每个组件对这个生命周期函数的定义不同,因此也比较麻烦!
如果说要优化的话,对应的CountDown组件也可能会成为别的组件的子组件,因此可以针对CountDown的父组件进行shouldComponentUpdate的设置:
shouldComponentUpdate(nextProps,nextState){
return nextProps.count !== this.state.count;
}
还有一个问题就是函数形式的子组件,为了代码清晰我们使用在jsx中定义的一个箭头函数的方式:
<CountDown startCount={10}>
count=><div>{count}</div>
</CountDown>
- 用起来非常方便,但是每次都是新的函数,this.porps.children和nextProps.props.children是否有必要性去比较?
- 虽然说需要比较,但是因为children是函数,因此不能够使用匿名函数! 因此该组件内部的函数应该是作为具名函数存在的!
<CountDown startCount={10}>
{showCount}
</CountDown>
const showCount = count=>{
return <div>{count}</div>
}
以函数为子组件,虽然存在对应的性能问题,但是确实是一个比较不错的方式,这种模式中代码的灵活性和性能两块需要作为开发者的我们进行好好地权衡!