React组件抽象

2,148 阅读7分钟

组件抽象指的是让不同组件公用同一类功能,可以说成组件功能复用,在不同的设计理念下,有许多抽象方法,而对于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>  
         );
    }
    }