React 高阶组件(HOC)入门指南

  以前的文章React Mixins入门指南介绍了React Mixin的使用。在实际使用中React Mixin的做用仍是很是强大的,可以使得咱们在多个组件中共用相同的方法。可是工程中大量使用Mixin也会带来很是多的问题。Dan Abramov在文章[Mixins Considered Harmful
](https://facebook.github.io/re...介绍了Mixin带来的一些问题,总结下来主要是如下几点:javascript

  • 破坏组件封装性: Mixin可能会引入不可见的属性。例如在渲染组件中使用Mixin方法,给组件带来了不可见的属性(props)和状态(state)。而且Mixin可能会相互依赖,相互耦合,不利于代码维护。html

  • 不一样的Mixin中的方法可能会相互冲突java

  为了处理上述的问题,React官方推荐使用高阶组件(High Order Component)react

高阶组件(HOC)

  刚开始学习高阶组件时,这个概念就透漏着高级的气味,看上去就像是一种先进的编程技术的一个深奥术语,毕竟名字里就有"高阶"这种字眼,实质上并非如此。高阶组件的概念应该是来源于JavaScript的高阶函数:git

高阶函数就是接受函数做为输入或者输出的函数github

  这么看来柯里化也是高阶函数了。React官方定义高阶组件的概念是:编程

A higher-order component is a function that takes a component and returns a new component.segmentfault

  (本人也翻译了React官方文档的Advanced Guides部分,官方的高阶组件中文文档戳这里)app

  这么看来,高阶组件仅仅只是是一个接受组件组做输入并返回组件的函数。看上去并无什么,那么高阶组件能为咱们带来什么呢?首先看一下高阶组件是如何实现的,一般状况下,实现高阶组件的方式有如下两种:ide

  1. 属性代理(Props Proxy)

  2. 反向继承(Inheritance Inversion)

属性代理

  又是一个听起来很高大上的名词,实质上是经过包裹原来的组件来操做props,举个简单的例子:

import React, { Component } from 'React';
//高阶组件定义
const HOC = (WrappedComponent) =>
  class WrapperComponent extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
}
//普通的组件
class WrappedComponent extends Component{
    render(){
        //....
    }
}

//高阶组件使用
export default HOC(WrappedComponent)

  上面的例子很是简单,但足以说明问题。咱们能够看见函数HOC返回了新的组件(WrapperComponent),这个组件原封不动的返回做为参数的组件(也就是被包裹的组件:WrappedComponent),并将传给它的参数(props)所有传递给被包裹的组件(WrappedComponent)。这么看起来好像并无什么做用,其实属性代理的做用仍是很是强大的。

操做props

  咱们看到以前要传递给被包裹组件WrappedComponent的属性首先传递给了高阶组件返回的组件(WrapperComponent),这样咱们就得到了props的控制权(这也就是为何这种方法叫作属性代理)。咱们能够按照须要对传入的props进行增长、删除、修改(固然修改带来的风险须要你本身来控制),举个例子:

const HOC = (WrappedComponent) =>
    class WrapperComponent extends Component {
        render() {
            const newProps = {
                name: 'HOC'
            }
            return <WrappedComponent
                {...this.props}
                {...newProps}
            />;
        }
    }

  在上面的例子中,咱们为被包裹组件(WrappedComponent)新增长了固定的name属性,所以WrappedComponent组件中就会多一个name的属性。

得到refs的引用

  咱们在属性代理中,能够轻松的拿到被包裹的组件的实例引用(ref),例如:

import React, { Component } from 'React';
 
const HOC = (WrappedComponent) =>
    class wrapperComponent extends Component {
        storeRef(ref) {
            this.ref = ref;
        }
        render() {
            return <WrappedComponent
                {...this.props}
                ref = {::this.storeRef}
            />;
        }
    }

  上面的例子中,wrapperComponent渲染接受后,咱们就能够拿到WrappedComponent组件的实例,进而实现调用实例方法的操做(固然这样会在必定程度上是反模式的,不是很是的推荐)。

抽象state

  属性代理的状况下,咱们能够将被包裹组件(WrappedComponent)中的状态提到包裹组件中,一个常见的例子就是实现不受控组件受控的组件的转变(关于不受控组件和受控组件戳这里)

class WrappedComponent extends Component {
    render() {
        return <input name="name" {...this.props.name} />;
    }
}

const HOC = (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} />;
        }
    }

  上面的例子中经过高阶组件,咱们将不受控组件(WrappedComponent)成功的转变为受控组件.

用其余元素包裹组件

  咱们能够经过相似:

render(){
        <div>
            <WrappedComponent {...this.props} />
        </div>
    }

  这种方式将被包裹组件包裹起来,来实现布局或者是样式的目的。

  在属性代理这种方式实现的高阶组件,以上述为例,组件的渲染顺序是: 先WrappedComponent再WrapperComponent(执行ComponentDidMount的时间)。而卸载的顺序是先WrapperComponent再WrappedComponent(执行ComponentWillUnmount的时间)。

反向继承

  反向继承是指返回的组件去继承以前的组件(这里都用WrappedComponent代指)

const HOC = (WrappedComponent) =>
  class extends WrappedComponent {
    render() {
      return super.render();
    }
  }

   咱们能够看见返回的组件确实都继承自WrappedComponent,那么全部的调用将是反向调用的(例如:super.render()),这也就是为何叫作反向继承。

渲染劫持

  渲染劫持是指咱们能够有意识地控制WrappedComponent的渲染过程,从而控制渲染控制的结果。例如咱们能够根据部分参数去决定是否渲染组件:

const HOC = (WrappedComponent) =>
  class extends WrappedComponent {
    render() {
      if (this.props.isRender) {
        return super.render();
      } else {
        return null;
      }
    }
  }

  甚至咱们能够修改修改render的结果:

//例子来源于《深刻React技术栈》

const HOC = (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;
    }
}
class WrappedComponent extends Component{
    render(){
        return(
            <input value={'Hello World'} />
        )
    }
}
export default HOC(WrappedComponent)
//实际显示的效果是input的值为"may the force be with you"

  上面的例子中咱们将WrappedComponent中的input元素value值修改成:may the force be with you。咱们能够看到先后elementTree的区别:
elementsTree:

elementsTree
newElementsTree:

newElementsTree

  在反向继承中,咱们能够作很是多的操做,修改state、props甚至是翻转Element Tree。反向继承有一个重要的点: 反向继承不能保证完整的子组件树被解析,开始我对这个概念也不理解,后来在看了React Components, Elements, and Instances这篇文章以后对这个概念有了本身的一点体会。
React Components, Elements, and Instances这篇文章主要明确了一下几个点:

  • 元素(element)是一个是用DOM节点或者组件来描述屏幕显示的纯对象,元素能够在属性(props.children)中包含其余的元素,一旦建立就不会改变。咱们经过JSXReact.createClass建立的都是元素。

  • 组件(component)能够接受属性(props)做为输入,而后返回一个元素树(element tree)做为输出。有多种实现方式:Class或者函数(Function)。

  因此, 反向继承不能保证完整的子组件树被解析的意思的解析的元素树中包含了组件(函数类型或者Class类型),就不能再操做组件的子组件了,这就是所谓的不能彻底解析。举个例子:

import React, { Component } from 'react';

const MyFuncComponent = (props)=>{
    return (
        <div>Hello World</div>
    );
}

class MyClassComponent extends Component{

    render(){
        return (
            <div>Hello World</div>
        )
    }

}

class WrappedComponent extends Component{
    render(){
        return(
            <div>
                <div>
                    <span>Hello World</span>
                </div>
                <MyFuncComponent />
                <MyClassComponent />
            </div>

        )
    }
}

const HOC = (WrappedComponent) =>
    class extends WrappedComponent {
        render() {
            const elementsTree = super.render();
            return elementsTree;
        }
    }

export default HOC(WrappedComponent);

element tree1
element tree2

  咱们能够查看解析的元素树(element tree),div下的span是能够被彻底被解析的,可是MyFuncComponentMyClassComponent都是组件类型的,其子组件就不能被彻底解析了。

操做props和state

  在上面的图中咱们能够看到,解析的元素树(element tree)中含有propsstate(例子的组件中没有state),以及refkey等值。所以,若是须要的话,咱们不只能够读取propsstate,甚至能够修改增长、修改和删除。

  在某些状况下,咱们可能须要为高阶属性传入一些参数,那咱们就能够经过柯里化的形式传入参数,例如:

import React, { Component } from 'React';

const HOCFactoryFactory = (...params) => {
    // 能够作一些改变 params 的事
    return (WrappedComponent) => {
        return class HOC extends Component {
            render() {
                return <WrappedComponent {...this.props} />;
            }
        }
    }
}

能够经过下面方式使用:

HOCFactoryFactory(params)(WrappedComponent)

  这种方式是否是很是相似于React-Redux库中的connect函数,由于connect也是相似的一种高阶函数。反向继承不一样于属性代理的调用顺序,组件的渲染顺序是: 先WrappedComponent再WrapperComponent(执行ComponentDidMount的时间)。而卸载的顺序也是先WrappedComponent再WrapperComponent(执行ComponentWillUnmount的时间)。

HOC和Mixin的比较

  借用《深刻React技术栈》一书中的图:
HOCandMixin

  高阶组件属于函数式编程(functional programming)思想,对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具备功能加强的效果。而Mixin这种混入的模式,会给组件不断增长新的方法和属性,组件自己不只能够感知,甚至须要作相关的处理(例如命名冲突、状态维护),一旦混入的模块变多时,整个组件就变的难以维护,也就是为何如此多的React库都采用高阶组件的方式进行开发。

相关文章
相关标签/搜索