React高阶组件与mixin使用

在多个不同的组件中需要用到相同的功能,其解决办法有两种:mixin和高阶组件。

1、mixin

mixin一直被广泛用于各种面向对象语言中,其作用是为单继承语言创造一种类似多重继承的效果。

广义的mixin方法,就是用赋值的方式将mixin对象中的方法都挂载到原对象上,来实现对象的混入,类似ES6中的Object.assign()的作用。原理如下:

const mixin = function(obj, mixins){
    const newObj = obj;
    newObj.prototype = Object.create(obj.prototype);

    for(let prop in mixins){ // 遍历mixins的属性
        if(mixins.hasOwnPrototype(prop)){ // 判断是否为mixin的自身属性
            newObj.prototype[prop] = mixins[prop]; // 赋值
        }
    }

    return newObj;
}

实质上就是把任意多个源对象拥有的自身可枚举属性复制给目标对象,然后返回目标对象。那么React中的mixin是这样的么?

在React中使用mixin

React在使用createClass构建组件时提供了mixin属性,比如官方封装的PureRenderMixin:

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
    mixins: [PureRenderMixin],

    render(){
        return <div>foo</div>;
    }
});

可以看出,mixins是一个数组,封装了我们需要的模块。不同mixin的方法或许会有重合,如何处理视重合部分是普通方法还是生命周期方法而定。

  • 在不同mixin里实现两个名字一样的普通方法:并不会覆盖,且控制台会报错。
  • 重合的是生命周期方法:将各个mixin的生命周期方法叠加在一起顺序执行。

可以看到,使用createClass实现的mixin为组件做了两件事:

  • 工具方法:mixin的基本功能。用来定义共享的工具类方法,以便在各个组件中使用。
  • 生命周期继承,props与state合并:mixin可以合并生命周期方法。如果有多个mixin定义了componentDidMount(),React会自动将它们合并处理。同样,mixin也可以作用在getInitialState的结果上,作state的合并,而props的合并也是这样的。

然而,使用ES6 classes构建组件时,并不支持mixin。这就不得不说到decorator语法糖。

ES6 classes 与 decorator

decorator是运用在运行时的方法,用以对组件进行“修饰”。现在,使用decorator来实现mixin:

function handleClass(target, mixins){
    if(mixins.length){
        for(let i=0, l=mixins.length; i<l; i++){
            // 获取mixins的attribute对象
            const decs = getOwnPropertyDescriptors(mixins[i]);
        }

        // 定义mixins的attribute对象
        for(const key in decs){
            if(!(key in target.prototype)){
                defineProperty(target.prototype, key, decs[key]);
            }
        }
    }
}

function mixin(...mixins){
    if(typeof mixins[0] === 'function'){
        return handleClass(mixins[0], []);
    }else{
        return target=>{
            return handleClass(target,mixins);
        }
    }
}

不难看出,这个mixin与本文开头createClass的mixin的实现是不一样的:createClass的mixin是直接给对象的prototype属性赋值,而这里是使用getOwnPropertyDescriptors和defineProperty进行定义。赋值与定义的区别在于赋值会覆盖已有的定义,而后者不会。两者在本质上都与官方的mixin方法存在区别,除了定义方法级别不能覆盖之外,还得加上对生命周期方法的继承以及对state的合并。

当然,decorator除作用在类上,还可以作用在方法上,但不在此处讨论。

minxin的缺陷

  • 破坏了原有组件的封装:可能会带来新的state和props,意味着会有些“不可见”的状态需维护。
  • 命名冲突:不同mixin中的命名不可知,故非常容易发生冲突,需要花一定成本解决。
  • 增加了复杂性,难以维护。

2、高阶组件

由于mixin存在上述缺陷,故React剥离了mixin。改用高阶组件来取代它。
高阶组件其实是一个函数,接收一个组件作为参数,返回一个新的组件作为返回值,类似于高阶函数。高阶组件和decorator是同一模式,因此,因此高阶组件可以作为decorator来使用。高阶组件基本形式:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

decorator形式:

@higherOrderComponent
WrappedComponent

高阶组件有以下好处:

  1. 适用范围广,它不需要es6或者其它需要编译的特性,有函数的地方,就有HOC。
  2. Debug友好,它能够被React组件树显示,所以可以很清楚地知道有多少层,每层做了什么。

高阶组件实现的方法有两种:

  1. 属性代理:通过被包裹组件的props来进行相关操作。主要进行组件的复用。
  2. 反向继承:继承被包裹的组件。主要进行渲染的劫持。

1、属性代理

属性代理主要是四个作用:操作props、通过refs访问组件实例、抽象state、使用其他元素包裹WrappedComponent。

(1)操作props
包括对props的读取、增加、删除、修改。删除和修改要注意不能影响原组件。
示例:增加一个props

function compHOC(WrappedComponent) {
  return class Comp extends React.Component {
    render() {
      const newProps = {
        user: currentLoggedInUser
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

(2)通过refs访问组件实例

可以通过ref回调函数的形式来访问传入组件的实例,进而调用组件相关方法或其他操作(如实例的props操作)。

//WrappedComponent初始渲染时候会调用ref回调,传入组件实例,在proc方法中,就可以调用组件方法
function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method()
    }

    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return <WrappedComponent {...props}/>
    }
  }
}

(3)抽象state
通过传入 props 和回调函数抽象state。高阶组件可以通过原组件抽象为展示型组件,分离内部状态。
示例:抽象 <Input />的 value 和 onChange 方法。

function compHOC(WrappedComponent) {
  return class Comp extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }

    // 将对name属性的onChange方法提取到此处=>提取到高阶组件,有效的抽象了同样的state操作
    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }
    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange
        }
      }
       return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

//使用方式如下
@compHOC
class Example extends React.Component {
  render() {
    //使用ppHOC装饰器之后,组件的props被添加了name属性,可以通过下面的方法,将 value 和 onChange方法 添加到input上面

    return <input name="name" {...this.props.name}/>

    // 变成<input name="name" value={this.state.name} onChange={this.onNameChange} />,这样我们就得到了一个受控组件。
  }
}

(4)使用其他元素包裹组件
用于加样式、布局等。

function compHOC(WrappedComponent) {
  return class Comp extends React.Component {
    render() {
      return (
        <div style={{display: 'block'}}>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
}

2、反向继承

高阶组件继承了WrappedComponent,意味着可以访问并使用WrappedComponent的state,props,生命周期和render方法,但它不能保证完整的子组件树被解析。如果在高阶组件中定义了与WrappedComponent中同名的方法,将会发生覆盖,就必须手动通过super进行调用。反向继承有两个比较大的特点:渲染劫持和控制state。

(1)渲染劫持
渲染劫持指的就是高阶组件可以控制 WrappedComponent 的渲染过程,并渲染各种各样的结果。我们可以在这个过程中在任何React元素输出的结果中读取、增加、修改、删除props,或读取或修改React元素树,或条件显示元素树,又或者是用元素包裹元素树。
大致形式如下:

function compHOC(WrappedComponent) {
  return class ExampleEnhance extends WrappedComponent {
    ...
    componentDidMount() {
      super.componentDidMount();
    }
    componentWillUnmount() {
      super.componentWillUnmount();
    }
    render() {
      ...
      return super.render();
    }
  }
}

例如,实现一个显示loading的请求。组件中存在网络请求,完成请求前显示loading,完成后再显示具体内容。(条件渲染)
可以用高阶组件实现如下:

function hoc(ComponentClass) {
    return class HOC extends ComponentClass { // 继承原组件
        render() {
            if (this.state.success) {
                return super.render()
            }
            return <div>Loading...</div>
        }
    }
}

@hoc
export default class ComponentClass extends React.Component {
    state = {
        success: false,
        data: null
    };
    async componentDidMount() {
        const result = await fetch(...请求);          
     this.setState({
            success: true,
            data: result.data
        });
    }
    render() {
        return <div>主要内容</div>
    }
}

正如前面所说,反向继承不能保证完整的子组件树被解析,这意味着会限制渲染劫持功能。渲染劫持的经验法则是:我们可以操控 WrappedComponent 的元素树,并输出正确的结果。但如果元素树中包括了函数类型的React组件,就不能操作组件的子组件。

(2)控制state
高阶组件可以读取,编辑和删除WrappedComponent实例的state,可以添加state。不过这个可能会破坏WrappedComponent的state,所以,要限制高阶组件读取或添加state,添加的state应该放在单独的命名空间里,而不是和WrappedComponent的state混在一起。
例如:通过访问WrappedComponent的props和state来做调试

export function IIHOCDEBUGGER(WrappedComponent) {
  return class II extends WrappedComponent {
    render() {
      return (
        <div>
          <h2>HOC Debugger Component</h2>
          <p>Props</p> <pre>{JSON.stringify(this.props, null, 2)}</pre>
          <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
          {super.render()}
        </div>
      )
    }
  }
}

3、组件命名

用HOC包裹的组件会丢失原先的名字,影响开发和调试。可以通过在WrappedComponent的名字上加一些前缀来作为HOC的名字,以方便调试。
参考react-redux实现:

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`;
//或
class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
  ...
}

//getDisplayName
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName ||
         WrappedComponent.name ||
         ‘Component’
}

4、组件参数

有时候,在调用高阶组件时,需要传入一些参数。可以这样实现:

function HocFactoryFactory(...params){
    // 可以做一些改变params的事
    return function HocFactory(WrappedCompinent){
        return class Hoc extends Component {
            render(){
                return <WrappedComponent {...this.props} />
            }
        }
    }
}

使用方式如下:

HocFactoryFactory(params)(WrappedComponent);

或者:

@HocFactoryFactory(params)
class WrappedComponent extends Component{
    ...
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 一、mixin 什么是mixin:创造一种类似多重继承的效果。事实上,说它是组合更为贴切。 1.封装mixin方法...
    南慕瑶阅读 2,015评论 0 0
  • 在目前的前端社区,『推崇组合,不推荐继承(prefer composition than inheritance)...
    Wenliang阅读 77,352评论 16 126
  • 事件系统 合成事件的绑定方式 Test 合成事件的实现机制:事件委派和自动绑定。 React合成事件系统的委托机制...
    cheneyg916阅读 365评论 0 1
  • 组件抽象指的是让不同组件公用同一类功能,可以说成组件功能复用,在不同的设计理念下,有许多抽象方法,而对于React...
    GIScyw阅读 676评论 0 0
  • title: react-高阶组件date: 2018-07-11 09:42:35tags: web 组件间抽象...
    Kris_lee阅读 25,954评论 2 21