组件抽象指的是让不同组件公用同一类功能,可以说成组件功能复用,在不同的设计理念下,有许多抽象方法,而对于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时产生的副作用。属性代理有几个常见的功能:
-
控制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} />; } }
-
通过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实例的引用。
-
抽象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} />; } }
-
使用其他元素包裹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> ); } }