React - 高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

高阶函数与高阶组件:

如果一个函数 接受一个或多个函数作为参数或者返回一个函数 就可称之为 高阶函数。

function withGreeting(greeting = () => {}) {
    return greeting;
}

高阶组件是参数为组件,并且返回值为新组件的一类函数。
它们都是一个函数。

function HigherOrderComponent(WrappedComponent) {
    return <WrappedComponent />;
}

当高阶组件中返回的组件是 无状态组件(函数组件)时,该高阶组件其实就是一个 高阶函数,因为 无状态组件 本身就是一个纯函数。

React 中的高阶组件主要有两种形式:属性代理 和 反向继承。

属性代理(Props Proxy):

简单例子:

// 无状态
function HigherOrderComponent(WrappedComponent) {
    return props => <WrappedComponent {...props} />;
}
// 有状态
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent {...this.props} />;
        }

    };
}

对于有状态属性代理组件来说,其实就是 一个函数接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件。

在有状态的属性代理高阶组件中可以进行一下操作:

  • 操作 props
    为 WrappedComponent 添加新的属性:
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            const newProps = {
                name: '天空明朗',
                age: 12,
            };
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    };
}
  • 抽离 state
    在返回组件中定义state和处理方法,通过props形式传递给参数组件,将参数组件中的state抽离到返回组件中做处理。
function withOnChange(WrappedComponent) {
    return class extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                value: '',
            };
        }
        onChange = (e) => {
            let value = e.target.value;
            this.setState({
                vaule,
            });
        }
        render() {
            const newProps = {
                value: this.state.name,
                onChange: this.onChange,
            };
            return <WrappedComponent {...this.props} {...newProps} />;
        }
    };
}

const NameInput = props => (<input {...props.name} />);
export default withOnChange(NameInput);

这样就将 input 转化成受控组件了。

  • 用其他元素包裹传入的组件 WrappedComponent
    给 WrappedComponent 组件包一层背景色:
function withBackgroundColor(WrappedComponent) {
    return class extends React.Component {
        render() {
            return (
                <div style={{ backgroundColor: '#828282' }}>
                    <WrappedComponent {...this.props} />
                </div>
            );
        }
    };
}
反向继承(Inheritance Inversion):

简单例子:

function HigherOrderComponent(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return super.render();
        }
    };
}

反向继承其实就是 一个函数接收一个 WrappedComponent 组件作为参数,并返回一个继承了参数 WrappedComponent 组件的类,且在该类的 render() 方法中返回 super.render() 方法。
属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent。

反向代理可以进行以下操作:

  • 操作state
    可以拿到 props 和 state 添加额外的元素
function withLogging(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            return (
                <div>
                    <p>state:</p>
                    <pre>{JSON.stringify(this.state)}</pre>
                    <p>props:</p>
                    <pre>{JSON.stringify(this.props)}</pre>
                    {super.render()}
                </div>
            );
        }
    };
}
  • 渲染劫持(Render Highjacking)
    条件渲染:通过 props.isLoading 这个条件来判断渲染哪个组件。
function withLoading(WrappedComponent) {
    return class extends WrappedComponent {
        render() {
            if(this.props.isLoading) {
                return <Loading />;
            } else {
                return super.render();
            }
        }
    };
}
高阶组件存在的问题:
  • 静态方法丢失
    因为原始组件被包裹于一个容器组件内,也就意味着新组件会没有原始组件的任何静态方法:
// 定义静态方法
WrappedComponent.staticMethod = function() {}

// 使用高阶组件

const EnhancedComponent = HigherOrderComponent(WrappedComponent);

// 增强型组件没有静态方法
typeof EnhancedComponent.staticMethod === 'undefined' // true

对静态方法进行拷贝:

function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    // 必须得知道要拷贝的方法
    Enhance.staticMethod = WrappedComponent.staticMethod;
    return Enhance;
}

但是这么做的一个缺点就是必须知道要拷贝的方法是什么,不过 React 社区实现了一个库 hoist-non-react-statics 来自动处理,它会 自动拷贝所有非 React 的静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
function HigherOrderComponent(WrappedComponent) {
    class Enhance extends React.Component {}
    hoistNonReactStatic(Enhance, WrappedComponent);
    return Enhance;
}
  • refs 属性不能透传
    一般来说高阶组件可以传递所有的 props 给包裹的组件 WrappedComponent,但是有一种属性不能传递,它就是 ref。与其他属性不同的地方在于 React 对其进行了特殊的处理。
    如果你向一个由高阶组件创建的组件的元素添加 ref 引用,那么 ref 指向的是最外层容器组件实例的,而不是被包裹的 WrappedComponent 组件。
    可以通过React 16.3中的一个名为 React.forwardRef 的 API 来解决这一问题:
function withLogging(WrappedComponent) {
    class Enhance extends WrappedComponent {
        render() {
            const {forwardedRef, ...rest} = this.props;
            // 把 forwardedRef 赋值给 ref
            return <WrappedComponent {...rest} ref={forwardedRef} />;
        }
    };
    // React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数

    // 所以这边的 ref 是由 React.forwardRef 提供的
    function forwardRef(props, ref) {
        return <Enhance {...props} forwardRef={ref} />
    }
    return React.forwardRef(forwardRef);
}

const EnhancedComponent = withLogging(SomeComponent);
  • 反向继承不能应用于函数组件的解析
    反向继承的渲染劫持可以控制 WrappedComponent 的渲染过程,也就是说这个过程中我们可以对 elements tree、state、props 或 render() 的结果做各种操作。但函数组件中不存在super.render()、state等功能。
高阶组件的约定
  • props 保持一致
    高阶组件在为子组件添加特性的同时,要尽量保持原有组件的 props 不受影响,也就是说传入的组件和返回的组件在 props 上尽量保持一致。
  • 你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例
  • 不要以任何方式改变原始组件 WrappedComponent
function withLogging(WrappedComponent) {
    WrappedComponent.prototype.componentWillReceiveProps = function(nextProps) {
        console.log('Current props', this.props);
        console.log('Next props', nextProps);
    }
    return WrappedComponent;
}

const EnhancedComponent = withLogging(SomeComponent);

会发现在高阶组件的内部对 WrappedComponent 进行了修改,一旦对原组件进行了修改,那么就失去了组件复用的意义,所以请通过 纯函数(相同的输入总有相同的输出) 返回新的组件

function withLogging(WrappedComponent) {
    return class extends React.Component {
        componentWillReceiveProps() {
            console.log('Current props', this.props);
            console.log('Next props', nextProps);
        }

        render() {
            // 透传参数,不要修改它
            return <WrappedComponent {...this.props} />;
        }
    };
}
  • 将返回组件接收的 props 给被包裹的组件 WrappedComponent
function HigherOrderComponent(WrappedComponent) {
    return class extends React.Component {
        render() {
            return <WrappedComponent name="name" {...this.props} />;
        }
    };
}
  • 不要再 render() 方法中使用高阶组件
class SomeComponent extends React.Component {
    render() {
        // 调用高阶函数的时候每次都会返回一个新的组件
        const EnchancedComponent = enhance(WrappedComponent);
        // 每次 render 的时候,都会使子对象树完全被卸载和重新
        // 重新加载一个组件会引起原有组件的状态和它的所有子组件丢失
        return <EnchancedComponent />;
    }
}
  • 使用 compose 组合高阶组件
// 不要这么使用
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// 可以使用一个 compose 函数组合这些高阶组件
// lodash, redux, ramda 等第三方库都提供了类似 `compose` 功能的函数
const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);

因为按照 约定 实现的高阶组件其实就是一个纯函数,如果多个函数的参数一样(在这里 withRouter 函数和 connect(commentSelector)所返回的函数所需的参数都是 WrappedComponent),所以就可以通过 compose 方法来组合这些函数。

高阶组件的应用场景:
  • 权限控制:
    利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别,这里以页面级别来举一个例子:
// HOC.js
function withAdminAuth(WrappedComponent) {
    return class extends React.Component {
        state = {
            isAdmin: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                isAdmin: currentRole === 'Admin',
            });
        }

        render() {
           if (this.state.isAdmin) {
                return <WrappedComponent {...this.props} />;
            } else {
                return (<div>您没有权限查看该页面,请联系管理员!</div>);
            }
        }
    };
}

然后是两个页面:

// pages/page-a.js
class PageA extends React.Component {
    constructor(props) {
        super(props);
        // something here...
    }
    componentWillMount() {
        // fetching data
    }
    render() {
        // render page with data
    }
}
export default withAdminAuth(PageA);

// pages/page-b.js
class PageB extends React.Component {
    constructor(props) {
        super(props);
        // something here...
    }
    componentWillMount() {
        // fetching data
    }
    render() {
        // render page with data
    }
}
export default withAdminAuth(PageB);

使用高阶组件对代码进行复用之后,可以非常方便的进行拓展,比如产品经理说,PageC 页面也要有 Admin 权限才能进入,我们只需要在 pages/page-c.js 中把返回的 PageC 嵌套一层 withAdminAuth 高阶组件就行,就像这样 withAdminAuth(PageC)。是不是非常完美!非常高效!!但是。。第二天产品经理又说,PageC 页面只要 VIP 权限就可以访问了。你三下五除二实现了一个高阶组件 withVIPAuth。

其实你还可以更高效的,就是在高阶组件之上再抽象一层,无需实现各种 withXXXAuth 高阶组件,因为这些高阶组件本身代码就是高度相似的,所以我们要做的就是实现一个 返回高阶组件的函数,把 变的部分(Admin、VIP) 抽离出来,保留 不变的部分,具体实现如下:

// HOC.js
const withAuth = role => WrappedComponent => {
    return class extends React.Component {
        state = {
            permission: false,
        }
        async componentWillMount() {
            const currentRole = await getCurrentUserRole();
            this.setState({
                permission: currentRole === role,
            });
        }

        render() {
            if (this.state.permission) {
                return <WrappedComponent {...this.props} />;
            } else {
                return (<div>您没有权限查看该页面,请联系管理员!</div>);
            }
        }
    };
}
withAuth(‘Admin’)(PageA);

有没有发现和 react-redux 的 connect 方法的使用方式非常像?没错,connect 其实也是一个 返回高阶组件的函数。

  • 页面复用
    假设我们有两个页面 pageA 和 pageB 分别渲染两个分类的电影列表,普通写法可能是这样:
// pages/page-a.js
class PageA extends React.Component {
    state = {
        movies: [],
    }
    // ...
    async componentWillMount() {
        const movies = await fetchMoviesByType('science-fiction');
        this.setState({
            movies,
        });
    }
    render() {
        return <MovieList movies={this.state.movies} />
    }
}
export default PageA;

// pages/page-b.js
class PageB extends React.Component {
    state = {
        movies: [],
    }
    // ...
    async componentWillMount() {
        const movies = await fetchMoviesByType('action');
        this.setState({
            movies,

        });
    }
    render() {
        return <MovieList movies={this.state.movies} />
    }
}
export default PageB;

页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:

const withFetching = fetching => WrappedComponent => {
    return class extends React.Component {
        state = {
            data: [],
        }
        async componentWillMount() {
            const data = await fetching();
            this.setState({
                data,
            });
        }
        render() {
        return <WrappedComponent data={this.state.data} {...this.props} />;
        }
    }
}

// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);

// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);

// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);

会发现 withFetching 其实和前面的 withAuth 函数类似,把 变的部分(fetching(type)) 抽离到外部传入,从而实现页面的复用。

装饰器模式:

高阶组件其实就是装饰器模式在 React 中的实现:通过给函数传入一个组件(函数或类)后在函数内部对该组件(函数或类)进行功能的增强(不修改传入参数的前提下),最后返回这个组件(函数或类),即允许向一个现有的组件添加新的功能,同时又不去修改该组件,属于 包装模式(Wrapper Pattern) 的一种。

什么是装饰者模式:在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为。

相比于使用继承,装饰者模式是一种更轻便灵活的做法。

总结:
  • 高阶组件不是组件,是一个把某个组件转换成另一个组件的函数
  • 高阶组件的主要作用是代码复用
  • 高阶组件是装饰器模式在React中的实现

参考链接:React 中的高阶组件及其应用场景

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

推荐阅读更多精彩内容

  • 在目前的前端社区,『推崇组合,不推荐继承(prefer composition than inheritance)...
    Wenliang阅读 77,665评论 16 125
  • React进阶之高阶组件 前言 本文代码浅显易懂,思想深入实用。此属于react进阶用法,如果你还不了解react...
    流动码文阅读 1,183评论 0 1
  • 高阶组件是react应用中很重要的一部分,最大的特点就是重用组件逻辑。它并不是由React API定义出来的功能,...
    叫我苏轼好吗阅读 898评论 0 0
  • 前言 学习react已经有一段时间了,期间在阅读官方文档的基础上也看了不少文章,但感觉对很多东西的理解还是不够深刻...
    Srtian阅读 1,654评论 0 7
  • 在多个不同的组件中需要用到相同的功能,这个解决方法,通常有Mixin和高阶组件。Mixin方法例如: 但是由于Mi...
    小鱼小虾小海洋阅读 817评论 0 3