组件间抽象

组件抽象指的是让不同组件公用同一类功能,可以说成组件功能复用,在不同的设计理念下,有许多抽象方法,而对于React,主要有两种:mixin和高阶组件。mixin在createClass中可以使用,但在ES6 classes中已抛弃(因为它存在很多副作用),但是我们可以通过decorator语法糖来封装mixin,这样就可以在ES6中使用mixin了。现在更常用的抽象方法是利用高阶组件的方式,它不仅可以减少代码量,而且可以把请求逻辑和展示逻辑分离到不同的层次上进行封装,从而为独立的管理和测试提供了更好的支持。

mixin方法

封装mixin方法

const mixin=function(obj,mixins){
    const newObj=obj;
    newObj.prototype=Object.create(obj.prototype);
    
    for(let prop in mixins){
      if(mixins.hasOwnProperty(prop)){
          newObj.prototype[prop]=mixins[prop];
       }
    }
    return newObj;
  }
  
  const BigMixin={
    fly:()=>{
    console.log('I can fly');
   }
 };
 
 const Big=function(){
  console.log('new big');
};

const FlyBig=mixin(Big,BigMixin);

const flyBig=new FlyBig();//=>'new big'
flyBig.fly();//=>'I can fly'

mixin方法就是用赋值的方法将mixin对象里的方法都挂载到原对象上,来实现对对象的混入。

在React中使用mixin

一个典型的例子,很多组件都会有定时更新界面的需求。首选我们要用setInterval()实现定时器的操作,还要及时清除定时器,以减小内存开销,尤其是大量的组件都包含定时器的时候,这时候我们可以用mixin共享机制来解决

var SetInterValMixin={
    componentWillMount:function(){
       this.intervals=[];
     },
     setInterval: function(){
       this.intervals.push(setInterval.apply(null,arguments));
     },
     componentWillUnmount: function(){
      this.intervals.map(clearInterval);
     }
};

val TickTock=React.createClass({
  mixins:[SetIntervalMixin],
  getInitialState: function(){
       return {seconds: 0};
  },
  componentDidMount: function(){
    this.setInterval(this.tick,1000);
  },
  tick: function(){
    return(
    <p>
      React已经运行了{this.state.seconds}秒.
    </p>
    );
}

});


React.render(
     <TickTock/>,
     document.getElementById('reactContainer')
);

简单的说,在mixin中定义的函数被混入组件实例中,多个组件定义相同的mixins则会使组件具有某些共同的行为。

SetIntervalMixin中也定义了componentWillMount函数,在这种情况下,React会优先执行mixin中的componentWillMount。如果mixin中定义了多个mixin,则会按声明的顺序依次执行,最后执行组件本身的函数。

如果一个组件使用了多个mixin,并且有多个minxin定义了同样的生命周期方法,所有这些生命周期方法都会执行到:首先按mixin中的引入顺序执行mixin里的方法,最后执行组件内定义的方法。

#在ES6中使用mixin

我们知道,ES6不支持mixin,但我们可以利用decorator来封装mixin。要在class的基础上封装mixin,就要说到class的本质,ES6并没有改变JavaScript面向对象方法基于原型的本质,不过在此之上提供了一些语法糖,class就是其中之一。所以我们也可以另一个语法糖decorator来实现class上的mixin。co-decorators库提供一些decorator,其中就实现@mixin:

import {getOwnPropertyDescriptors} from './private/utils';

const {defineProperty}=Object;

function handleClass(target,mixins){
  if(!mixins.length){
    throw new SyntaxError('@mixin() class ${target.name} requires at least one mixin as an argument');
}

for(let i=0,l=mixins.length;i<l;i++){
   const decs=getOwnPropertyDescriptors[mixins[i]]);
   
   for(const key in decs){
      if(!(key in target.prototype)){
         defineProperty(target.protype,key,decs[key]);
      }
  }
 }
}

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

思路是将每一个mixin对象的方法都叠加到target对象的原型上,注意defineProperty这个方法,它是定义而不是像之前的赋值(官方的createClass中的mixin方法),定义不会覆盖已有方法,但赋值会。

使用@mixin:

import React, {component} from 'React';
import {mixin} from 'core-decorators';

const PureRender={
   shouldComponentUpdate(){}
}

const Theme={
    setTheme(){}
}

@mixin(PureRender,Theme)
class MyComponent extends Component{
    render(){}
}

mixin的问题

  • 破坏原有组件的封装

  • 命名冲突

  • 增加复杂性

    针对这些问题,React社区提出了高阶组件的方式来取代mixin。

高阶组件(HOC)

当React组件被包裹时,高阶组件会返回一个增强(enhanced )的React组件。实现高阶组件有两种方法,属性代理和反向继承。高阶组件的功能简单说就是用力控制被包裹的组件,然后就可以做一些有意思的事情,如控制被包裹组件的state,props,抽象被包裹组件(可以将被包裹组件抽象成展示型组件),还可以翻转元素树等等,而且不会产生副作用。

属性代理

指的是通过高阶组件将props传递给被包裹的React组件,对于原始组件来说,并不会感知到高阶组件的存在,只需要把功能套在它之上就可以了。从而避免了使用mixin时产生的副作用。属性代理有几个常见的功能:

  1. 控制props

    我们可以读取,增加,编辑或删除从WrappedComponent传进来的props,但需要小心删除和编辑重要的props。

    import React,{Component} from 'React';
    
    const MyContainer=(WrappedComponent)=>
       class extends Component{
           render(){
               const newProps={
                   text:newText,
               };
               return <WrappedComponent {...this.props} {...newProps} />;
           }
       }
    
  2. 通过refs使用引用

    在高阶组件中,我们可以接受refs使用WrappedComponent的引用。

    import React,{component} from 'React';
    
    const MyContainer=(WrappedComponent)=>
       class extends Component{
           proc(wrappedComponentInstance){
               wrapperComponentInstance.method();
           }
       
       render(){
           const props=object.assign({},this.props,{
               ref:this.proc.bind(this),
           })
           return <WrappedComponent {...props} />;
           }
       }
    

    当WrappedComponent被渲染时,refs回调函数就会被执行,这样就会拿到一份WrappedComponent实例的引用。

  3. 抽象state

    即高阶组件可以将原组件抽象为展示型组件,分离内部状态,我们可以通过WrappedComponent提供的props和回调函数抽象state。意思就是讲原组件的state抽象到高阶组件中(放到外面去了)。

    import React,{Component} from 'React';
    
    const MyContainer=(WrappedComponent)=>
      class extends Component{
          constructor(props){
              super(props);
              this.state={
                  name:''.
              };
              
              this.onNameChange=this.onNameChange.bind(this);
          }
           onNameChange(event){
               this.setState({
                   name:event.target.value,
               })
           }
           
           render(){
               const newProps={
                   name:{
                       value:this.state.name,
                       onChange:this.onNameChange,
                   },
               }
               return <WrappedComponent {...this.props} {...newProps} />;
           }
      }
    

    我们把input组件中对name prop的onChange方法提取到高阶组件中,这样就有效地抽象了同样的state操作。可以这么来使用它:

    import React,{Component} from 'React';
    
    @MyContainer
    class MyComponent extends Component{
        render(){
            return <input name="name" {...this.props.name} />;
        }
    }
    

  4. 使用其他元素包裹WrappedComponent

    例如我们可以加一个样式

    import React,{Component} from 'React';
    
    const MyContainer=(WrappedComponent)=>
      class extends Component{
          render(){
              return(
              <div style={{display:'block'}}>
                 <WrappedComponent {...this.props} />
               </div>
              )
          }
      }
    

    反向继承

    const MyContainer=(WrappedComponent)=>
       class extends WrappedCompoennet{
           render(){
               return super.render();
           }
       }
    

    高阶组件返回的组件继承于WrappedComponent,它可以使用WrappedComponent的state、props、生命周期和render方法,但它不能保证完整的子组件被解析,因为被动的继承了WrappedComponent,所有的调用都会反向。它有两个特点,渲染劫持和控制state。

    渲染劫持

    指的是高阶组件可以控制WrappedComponent的渲染过程我们可以在这个过程中在任何React元素输出的结果中读取、增加、修改、删除props,或读取或修改React元素树,或条件显示元素树,又或是用样式控制包裹元素树。

    //条件渲染
    const MyContainer=(WrappedComponent)=>
      class extends WrappedComponent{
          render(){
              if(this.props.loggedIn){
                  return super.render();
              }
              else{
                  return null;
              }
          }
      }
    
    //对render的输出结果进行修改
    const MyContainer=(WrappedComponent)=>
       class extends wrappedComponent{
           render{
               const elementsTree=super.render();
               let newProps={};
               
               if (elementsTree && elementsTree.type==='input'){
                   newProps={value:'may the force be with you'};
               }
               const props=Object.assign({},elementsTree.props,newProps);
               const newElementsTree =React.cloneElement(
               elementsTree,props,elementsTree.props.children);
               return newElementsTree;           
           }
       }
    

    可以看到,顶层的input组件的value被改写成'may the force be with you'。所以呢,我们可以做各种各样的事,甚至可以反转元素树,或是改变元素树中的props,这也是Radium库构造的方法。

    控制state

    高阶组件可以读取、修改或删除WrappedComponent实例中的state,如果需要的话,也可以增加state,但是这样做组件状态会变得混乱,大部分的高阶组件应该限制读取或增加state。

    const MyContainer=(WrappedComponent)=>
      class extends WrappedComponent{
       render(){
         return(
         <div>
           <h2>HOC  Debuger Component</h2>
           <p>Props</p> <pre>{JSON.stringfy(this.props,null,2)}</pre>
           <p>state</p><pre>{JSON.stringfy(this.state,null,2)}</pre>
           {super.render()}
         </div>  
         );
    }
    }
    

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

推荐阅读更多精彩内容