在多个不同的组件中需要用到相同的功能,其解决办法有两种: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
高阶组件有以下好处:
- 适用范围广,它不需要es6或者其它需要编译的特性,有函数的地方,就有HOC。
- Debug友好,它能够被React组件树显示,所以可以很清楚地知道有多少层,每层做了什么。
高阶组件实现的方法有两种:
- 属性代理:通过被包裹组件的props来进行相关操作。主要进行组件的复用。
- 反向继承:继承被包裹的组件。主要进行渲染的劫持。
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{
...
}
网友评论