React高阶组件探究

React高阶组件探究

在使用React构建项目的过程中,经常会碰到在不同的组件中需要用到相同功能的情况。不过我们知道,去这些组件中到处编写同样的代码并不优雅。

在以往,为了解决以上的情况,我们会用到Mixin这种方式来解决问题。

以下是一个最为简单的Mixin

var defaultMixin = {
    getDefaultProps: function() {
        return {
            name: "Allen"
        }
    }
}

var Component = React.createClass({
    mixins: [defaultMixin],
    render: function() {
        return <h1>Hello, {this.props.name}</h1>
    }
})

这个例子很好理解,在这里就不详述了。要使用mixin属性,我们只需要简单地在组件中加入mixins属性即可。

不过在React ES6中,因为种种原因,mixin将不再被支持。

详情可以参考以下文章:

  1. Mixin已死,Composition 万岁

  2. React官方在ES6中不建议使用mixin

文章中提到用高阶组件来替代mixin
所谓高阶组件其实只是一个方法。这个方法中返回一个包裹着原组件的新组件。接下来看看它的基本用法。

高阶组件基本介绍

function hoc(ComponentClass) {
    return class HOC extends React.Component {
        componentDidMount() {
            console.log("hoc");
        }
    
        render() {
            return <ComponentClass />
        }
    }
}

以上代码可以看作是一个最简的高阶组件

使用起来也很简单,比如有一个React组件。

class ComponentClass extends React.Component {
    render() {
        return <div></div>
    }
}

export default hoc(MyComponent);

只需要将你的组件类作为参数传入高阶组件这个方法中即可。

如果采用ES7的decorator语法。则可以更简洁

@hoc
export default class ComponentClass extends React.Component {
    //...
}

简单来说,高阶组件就是一个函数,它让你传入一个组件类,然后返回给你一个更强大的组件类。

用更生动一点的说法,高阶组件就像是一个buff……

鸡血(大锤)  ===>  打了鸡血的大锤

@超级buff
class 赛亚人 {}   //超级赛亚人get~~~

类似以上这种感觉……

高阶组件中的props、ref、state

操作props###

你可以在高阶组件中对ComponentClass中的props进行读取、添加、编辑操作。

例子:

function hoc(ComponentClass) {
    return class HOC extends React.Component {
        render() {
            const newProps = {
                name: "cqm",
                value: "testData",
            }
            return <ComponentClass {...this.props} {...newProps} />
        }
    }
}

高阶组件中能够通过this.props直接获取到ComponentClass中的props,假设this.props中有name,那么newProps中的name会覆盖掉this.props中的name。

通过ref访问组件实例###

你可以通过ref访问到组件中的实例。可以看以下例子:

function hoc(ComponentClass) {
    return class HOC extends React.Component {
        
        componentDidMount() {
            console.log(this.refs.componentClass);
        }
    
        render() {
            return <ComponentClass ref="componentClass" />
        }
    }
}

提示:React中ref还有这种回调函数的写法: ref={(dom) => this.dom = dom} 依稀记得好像在哪里看到过,说这种写法更新更好

你也可以使用另一种方式:

function hoc(ComponentClass) {
    return class HOC extends React.Component {
        
        getDom(dom) {
            console.log(dom);
        }
        
        render() {
            return <ComponentClass ref={(dom) => this.getDom(dom)} />
        }
    }
}

ref回调函数在ComponentClass渲染时执行,从而可以轻松拿到它的引用。

获取state###

在高阶组件中获取state有若干种方式,下面将一一阐述。

和React父子组件交互的方式类似,你可以往ComponentClass中传入一个函数,之后ComponentClass中通过props拿到这个函数,往里面传入你想要的state参数。

示例如下:

function hoc(ComponentClass) {
    return class HOC extends React.Component {
        
        getState(state) {
            console.log(state);
        }
        
        render() {
            const newProps = {
                getState: (state) => this.getState(state)
            };
            return <ComponentClass {...newProps} />
        }
    }
}

@hoc
export default class ComponentClass extends React.Component {
    state = {
        value = ""
    };
    
    render() {
        return (
            <div>
                <input onChange={(z) => this.props.getState(z.target.value)} />
            </div>
        );
    }
}

理论上你还可以通过this.refs.componentClass.state这种方式来获取全部的state(不过我一般不这么干)。


接下来用到的获取state的方式应该算是高阶组件的另一种运用方式

我在文章开篇提到过这句话

所谓高阶组件其实只是一个方法。这个方法中返回一个包裹着原组件的新组件。接下来看看它的基本用法。

高阶组件中所谓的包裹方式主要有以下两种:

  1. Props Proxy: 高阶组件通过ComponentClass中的props来进行相关的操作。

  2. Inheritance Inversion: 高阶组件继承自ComponentClass。

之前我们并未用到第二种包裹方式,大部分采用的Props Proxy的方式来进行操作。接下来我们来详细讲讲第二种方式。

Inheritance Inversion

Inheritance Inversion的最简实现大概如下:

function hoc(ComponentClass) {
    return class HOC extends ComponentClass {
        render() {
            return super.render();
        }
    }
}

高阶组件hoc中,返回了一个继承了原先组件类的组件。颇有一种反转的味道。

显而易见,你可以在这个组件类中拿到一切ComponentClass的props、state。

代码如下:

function hoc(ComponentClass) {
    return class HOC extends ComponentClass {
        
        componentDidMount() {
            console.log(this.state);
        }
    
        render() {
            return super.render();
        }
    }
}

@hoc
export default class ComponentClass extends React.Component {
    
    state = {
        id: 0,
        value: "hahaha"
    };
    
    render() {
        return <div>ComponentClass</div>
    }
}

你也可以通过super.[lifecycle]来调取ComponentClass的声明周期或是render函数

接下来我们可以来实现一个简单的需求。比如说一个组件中存在网络请求,你希望在请求完成之前组件显示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>
    }
}

这个例子应该很好理解。这边就不再详细解释了。

Inheritance Inversion渲染劫持

渲染劫持这句话的意思很好理解。因为在高阶组件中你可以控制ComponentClass的渲染输出,因此你可以在渲染的时候做出各种各样的事情。比如修改props,修改render的组件树等等。

以下示例用以演示修改render方法输出的组件树

function hoc(ComponentClass) {
    return class HOC extends ComponentClass {
        render() {
            const elementTree = super.render();
            elementTree.props.children = elementTree.props.children.filter((z) => {
                return z.type !== "ul" && z;
            }
            const newTree = React.cloneElement(elementTree);
            return newTree;
        }
    }
}

@hoc
export default class ComponentClass extends React.Component {
    render() {
        const divStyle = {
            width: '100px',
            height: '100px',
            backgroundColor: 'red'
        };
        
        return (
            <div>
                <p style={{color: 'brown'}}>啦啦啦</p>
                <ul>
                    <li>1</li>
                    <li>2</li>
                </ul>
                <h1>哈哈哈</h1>
            </div>
        )
    }
}

以上就是一个渲染劫持的示例,我将ComponentClass中的ul标签给移除了。你可以使用渲染劫持完成类似的需求。如果你对于React中的组件元素操作不是很了解的话,推荐您去看看官方文档中的这一节:

React官方关于component-elements的文档

实际案例

mobx-react

/**MyStore文件**/
import { observable } from 'mobx';

export class MyStore() {
    @observable value = 1;
    
    @action add = () => {
        this.value++;
    }
}

/**另一个文件**/
import MyStore from './MyStore';

const store = new MyStore();

@observer
export default class MyComponent extends React.Component {
    render() {
        return (
            <div>
                <p>这只青蛙的寿命{store.value}</p>
                <button onClick={store.add}>+1s</button>
            </div>
        );
    }
}

可以把observer当成高阶组件。传入MyComponent这个类,之后mobx-react会对其生命周期进行各种处理,并通过调用实例的forceUpdate来进行刷新实现最小粒度渲染。

算是小小安利一下mobx这个框架吧。

顺便一提, mobx中提倡一份数据引用,而在redux中则更提倡immutable思想,返回新对象。

pure-render-decorator

@pureRender
export default class MyComponent extends React.Component {
    
    static propTypes = {
        name: React.PropTypes.string,
    };
    
    render() {
        return (
            <div>{this.props.name}</div>
        );
    }
}

pureRender中重新定义了shouldComponentUpdate这一生命周期,当传来的props未发生改变时,不重新render,从而达到性能优化的目的。


以上。。。

References

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

推荐阅读更多精彩内容