React 高阶组件

高阶组件是对既有组件进行包装,以增强既有组件的功能。其核心实现是一个无状态组件(函数),接收另一个组件作为参数,然后返回一个新的功能增强的组件。
看一个最简单的例子:
App.js:

import React,{ PureComponent } from "react";

const DisplayUser = (props) => {
    return(
        <div>
            姓名:{ props.username }
        </div>
    );
} 

const HOC = (InnerComponent) => {
    return class HOCComponent extends PureComponent{
        render(){
            const { username } = this.props;
            return(
                <InnerComponent username = { username } />
            );
        }
    }
}

export default HOC(DisplayUser);

index.js:

import React from "react";
import ReactDOM from "react-dom";
import HOC from "./App";

ReactDOM.render(<HOC username = "Mike" birth = "1999-09-09" />,document.getElementById("root"));

HOC 函数接受一个组件作为参数,返回一个新的组件,新组件中渲染了参数组件,这样就可以对参数组件做一些配置,例如本例的过滤 props,只传递给参数组件合法的 props,不传递非法的 props。

高阶组件的分类

高阶组件分为两种类型:

  • 代理型高阶组件
  • 继承型高阶组件

上面的例子中的高阶组件类型就是代理型高阶组件。
二者的主要区别是对参数组件的操作上,代理型高阶组件是返回一个新的组件类,该类继承于根组件(Component 或 PureComponent),然后让这个新的组件类去渲染参数组件,这样就可以对参数组件实现包装或代理 props 的操作。继承型高阶组件也是返回一个新的组件类,但这个新的类不继承根组件类,而是继承于参数组件类,这样我们就可以重写参数组件类中的一些方法。
这里的高阶组件应该叫做高阶组件生成函数,其执行的返回值才叫高阶组件,为了方便我们这里统一称为高阶组件了,请知悉。
下面分别来说下这两类的高阶组件。

代理型高阶组件

代理型高阶组件接受一个组件作为参数,返回一个新的继承于根组件(Component、PureComponent)的组件,并用该新组件渲染参数组件,其基本结构为:

export const HOC = (SomeComponent) => {
    return class HOCComponent extends PureComponent{
        render(){
            doSomeThing()...
            return(
                <SomeComponent />
            );
        }
    }
}

代理型高阶组件有以下几个作用:

  • 代理 props
  • 访问 ref
  • 抽取状态
  • 包装组件

代理 props

代理型高阶组件可以接收被包装组件的 props,并对这些 props 进行操作,如增删改操作。在渲染被包装组件时,传入被修改的 props,如本文开头的例子。
基本结构如下:

export const HOC = (SomeComponent) => {
    return class HOCComponent extends PureComponent{
        render(){
            const newProps = doSomeThing(this.props);
            return(
                <SomeComponent {...newProps} />
            );
        }
    }
}

访问 ref

ref 是一个特殊的属性,可以声明为函数,如果 ref 属性是一个函数,那么将在组件装载完成后自动执行该函数,并将当前组件的实例传入该函数中
看一个栗子:

class SubComponent extends PureComponent{
    render(){
        return(
            <div className = "box">
                我想要被获取!!!
            </div>
        );
    }
}

const HOC = (InnerComponent) => {
    return class HOCComponent extends PureComponent{
        constructor(...args){
            super(...args);
            this.handSubRef = this.handSubRef.bind(this);
        }
        
        // 获取子组件的实例
        // 该函数在子组件被装载后自动调用
        handSubRef(subEle){
            console.log(subEle);
        }

        render(){
            return(
                <InnerComponent ref = { this.handSubRef } />
            );
        }
    }
}

export default HOC(SubComponent);

这样,在参数组件被装载后,就会自动执行 handSubRef 函数,并将参数组件的实例传入 handSubRef 函数。在 handSubRef 中可以获取参数组件的一些列属性,如 state、props 等。
注意,如果参数组件是一个无状态组件,那么传入 handSubRef 的参数将是 null

const SubComponent = (props) => {
    return(
        <div className = "box">
            我想要被获取!!!
        </div>
    );
} 

const HOC = (InnerComponent) => {
    return class HOCComponent extends PureComponent{
        constructor(...args){
            super(...args);
            this.handSubRef = this.handSubRef.bind(this);
        }
        
        // 参数组件是无状态组件
        // subEle 为 null
        handSubRef(subEle){
            console.log(subEle);
        }

        render(){
            return(
                <InnerComponent ref = { this.handSubRef } />
            );
        }
    }
}

export default HOC(SubComponent);

抽取状态

当一个组件的功能较复杂时,我们建议将组件拆分为容器组件和UI组件,UI组件负责展示,容器组件用来进行逻辑管理。这个步骤就是“抽取状态”。
我们将前面的计算器应用中的组件进行一些修改:

...
const UICounter = (props) => {
    const { increase,decrease,value} = props;
    return(
        <div className = "counter">
            <div>
                <button onClick = { increase }>+</button>
                <button onClick = { decrease }>-</button>
            </div>
            <span>当前的值为:{ value }</span>
        </div>
    );
}

const HOC = (SubEle) => {
    return class HOCComponent extends PureComponent{
        constructor(props) {
            super(props);
            // 获取初始状态
            this.state = {
                value:store.getState().value,
            };
        }

        componentWillMount() {
            // 监听 store 变化
            store.subscribe(this.watchStore.bind(this));
        }

        componentWillUnmount() {
            // 对 store 变化取消监听
            store.unsubscribe(this.watchStore.bind(this));
        }

        // 监听回调函数,当 store 变化后执行
        watchStore(){
            // 回调函数中重新设置状态
            this.setState(this.getCurrentState());
        }

        // 从 store 中获取状态
        getCurrentState(){
            return{
                value:store.getState().value,
            }
        }

        // 增加函数
        increase(){
            // 派发 INCREMENT Action
            store.dispatch(ACTIONS.increament());
        }

        // 减少函数
        decrease(){
            // 派发 DECREAMENT Action
            store.dispatch(ACTIONS.decreament());
        }

        render(){
            return(
                <UICounter
                    increase = { this.increase.bind(this)}
                    decrease = { this.decrease.bind(this)}
                    value = { this.state.value }
                />
            );
        }
    }
}

export default HOC(UICounter);
...

包装组件

包装组件就是对某些组件进行组合,返回不同的组合形式,同时可以设置展示样式。如将展示性 select 和输入表单包装成可搜索的 select等。
基本结构如下:

// 可以传入一个数组,或者单个组件
const HOC = (SubComponents) => {
    return class HOCComponent extends PureComponent{
        render(){
            const style = ...
            // 将子组件组合成一个大组件,同时可以修改样式
            return(
                <div style = { style }>
                    {...SubComponents}
                </div>
            );
        }
    }
}

export default HOC([Select,SearchBox]);

至此,代理型高阶组件就说完了,下面说说继承型高阶组件。

继承型高阶组件

和代理型高阶组件创建一个继承于根组件(Component、PureComponent)的组件类不同的是,继承型高阶组件是创建一个继承于参数组件的组件类,以此可以覆写父组件中的一些方法。
最常用的应用是重写父组件的生命周期函数:


const HOC = (ParComponents) => {
    return class HOCComponent extends ParComponents{
        // 覆写父组件的生命周期函数
        shouldComponentUpdate(nextProps, nextState) {
            ...
        }

        render(){
            // 渲染时仍然按照父组件的 render 函数进行渲染
            return super.render();
        }
    }
}

export default HOC(ParComponents);

子组件覆写了父组件的生命周期方法,以获取更好的渲染性能,在渲染(render)时,还是调用父类的 render 方法。

函数作为子组件

前面用高阶组件实现了代理 props 功能,对于特定的组件,需要传入特定的 props 名,由于每个组件需要的 props 可能有差异,如果使用高阶组件对参数组件进行判断,再传入合适的 props 名显然太麻烦了。这时如果我们需要更通用的方案,可以接收函数作为子组件,利用函数的形参的特性来排除组件 props 名之间的差异性。看一下演示代码:

...
// 此组件需要使用 user props
const SubComponent1 = (user) => {
    return(
        <TestComponent user = { user } />
    );
} 

// 此组件需要使用 username props
const SubComponent2 = (user) => {
    return(
        <TestComponent2 username = { user } />
    );
} 

// 高阶组件接受 testUser 作为 props
const HOC = (props) => {
    const { testUser } = props;
    return(
        props.children(testUser)
    );
}
...

使用此高阶组件传递 props:

...
<HOC testUser = "MIKE">
    { SubComponent1 }
</HOC>

<HOC testUser = "JACK">
    { SubComponent2 }
</HOC>
...

高阶组件接收一个函数作为子组件,然后调用这个组件函数,并传入相应的 props,函数调用的结果返回一个组件,组件需要接收什么样的 props 名可以自行定义,不用在高阶组件中进行判断了。
以函数作为组件的形式主要用于不同的组件需要接收同一份 props,但是它们需要的 props 名称不一样的情况,可以用函数的形参巧妙的化解掉父组件的判断。虽然用途较少,但不失为一个优秀的模式,将 children 进行调用简直是神来之笔有木有。

总结

本文谈到了 React 中的高阶组件,高阶组件主要包括两种类型:

  • 代理型高阶组件
  • 继承型高阶组件

代理型高阶组件的主要用途是:

  • 代理子组件的 props(对 props 进行增删改操作)
  • 获取子组件的 ref(实例)
  • 抽取子组件状态(拆分逻辑)
  • 将子组件进行包装(组合子组件,调整样式等)

继承型高阶组件的主要用途是覆写父组件的生命周期方法,通常是 shouldComponentUpdate 方法,以此来提高渲染性能,渲染时调用父类的 render 函数(super.render())。
最后,介绍了函数作为子组件的情形,主要用于不同的组件接受同一份 props,但是它们各自需要的 props 名不同的情况,利用函数的形参巧妙化解父组件的额外判断操作。这是一种优秀的模式,让人耳目一新。

完。

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

推荐阅读更多精彩内容

  • 在目前的前端社区,『推崇组合,不推荐继承(prefer composition than inheritance)...
    Wenliang阅读 77,648评论 16 125
  • 什么是高阶组件? high-order-function(高阶函数)相信大多数开发者来说都熟悉,即接受函数作为参数...
    哇塞田阅读 9,062评论 9 23
  • React高阶组件探究 在使用React构建项目的过程中,经常会碰到在不同的组件中需要用到相同功能的情况。不过我们...
    绯色流火阅读 2,567评论 4 19
  • 提到高阶组件,不由得想起了函数式编程的高阶函数,高阶函数就是指:接受函数作为输入,或者输出一个函数。例如map,s...
    Dabao123阅读 1,581评论 0 1
  • “ 这里的书种类不少,你想买那一类的呢?”在介绍书之前,他必须要明确来的人想买那一类的书,他才肯细致的介绍。 在大...
    怀昙忆蝶阅读 150评论 9 2