高阶组件(Higher Order Component)

高阶组件(HOC)是React开发中的特有名词,一个函数返回一个React组件,指的就是一个React组包裹着另外一个React组件。能够理解为一个生产React组件的工厂。javascript

有两种类型的HOC:html

  1. Props Proxy(pp) HOC对被包裹组件WrappedComponent的props进行操做。
  2. Inherbitance Inversion(ii)HOC继承被包裹组件WrappedComponent

Props Proxy

一种最简单的Props Proxy实现java

function ppHOC(WrappedComponent) {  
  return class PP extends React.Component {    
    render() {      
      return <WrappedComponent {...this.props}/>    
    }  
  } 
}

这里的HOC是一个方法,接受一个WrappedComponent做为方法的参数,返回一个PP class,renderWrappedComponent。使用的时候:react

const ListHOCInstance = ppHOC(List)
<ListHOCInstance name='instance' type='hoc' />

这个例子中,咱们将本应该传给List的props,传给了ppHoc返回的ListHOCInstance(PP)上,在HOC内部咱们将PP的参数直接传给ListWrappedComponent)。这样的就至关于在List外面加了一层代理,这个代理用于处理即将传给WrappedComponent的props,这也是这种HOC为何叫Props Proxy。git

在pp中,咱们能够对WrappedComponent进行如下操做:es6

  1. 操做props(增删改)
  2. 经过refs访问到组件实例
  3. 提取state
  4. 用其余元素包裹WrappedComponent

操做props

好比添加新的props给WrappedComponentgithub

const isLogin = false;

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      const newProps = {
        isNew: true,
        login: isLogin
      }
      return <WrappedComponent {...this.props} {...newProps}/>
    }
  }
}

WrappedComponent组件新增了两个props:isNew和login。redux

经过refs访问到组件实例

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}/>
    }
  }
}

Ref 的回调函数会在 WrappedComponent 渲染时执行,你就能够获得WrappedComponent的引用。这能够用来读取/添加实例的 props ,调用实例的方法。segmentfault

不过这里有个问题,若是WrappedComponent是个无状态组件,则在proc中的wrappedComponentInstance是null,由于无状态组件没有this,不支持ref。api

提取state

你能够经过传入 props 和回调函数把 state 提取出来,

function ppHOC(WrappedComponent) {
  return class PP extends React.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}/>
    }
  }
}

使用的时候:

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

export default ppHOC(Test);

这样的话,就能够实现将input转化成受控组件。

用其余元素包裹 WrappedComponent

这个比较好理解,就是将WrappedComponent组件外面包一层须要的嵌套结构

function ppHOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      return (
        <div style={{display: 'block'}}>
          <WrappedComponent {...this.props}/>
        </div>
      )
    }
  }
}

Inheritance Inversion(反向继承)

先看一个反向继承(ii)的?:

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

上面例子能够看出来Enhancer继承了WrappedComponent,可是Enhancer能够经过super关键字获取到父类原型对象上的全部方法(父类实例上的属性或方法则没法获取)。在这种方式中,它们的关系看上去被反转(inverse)了。

咱们可使用Inheritance Inversion实现如下功能:

  1. 渲染劫持(Render Highjacking)
  2. 操做state

渲染劫持

之因此被称为渲染劫持是由于 HOC 控制着 WrappedComponent 的渲染输出,能够用它作各类各样的事。经过渲染劫持咱们能够实现:

  1. 在由 render输出的任何 React 元素中读取、添加、编辑、删除 props
  2. 读取和修改由 render 输出的 React 元素树
  3. 有条件地渲染元素树
  4. 把样式包裹进元素树(就像在 Props Proxy 中的那样)

demo1:条件渲染

function iiHOC(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      if (!this.props.loading) {
        return super.render()
      } else {
        return <div>loading</div>    
      }
    }
  }
}

demo2:修改渲染

function iiHOC(WrappedComponent) {
  return class Enhancer 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
    }
  }
}

在这个例子中,若是 WrappedComponent 的输出在最顶层有一个 input,那么就把它的 value 设为 “may the force be with you”

你能够在这里作各类各样的事,你能够遍历整个元素树,而后修改元素树中任何元素的 props。

操做state

HOC 能够读取、编辑和删除 WrappedComponent 实例的 state,若是你须要,你也能够给它添加更多的 state。记住,这会搞乱 WrappedComponent 的 state,致使你可能会破坏某些东西。要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 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>
      )
    }
  }
}

问题

使用高阶组件的时候,也会遇到一些问题:

静态方法丢失

当使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的全部静态方法。

可使用hoist-non-react-statics来帮你自动处理,它会自动拷贝全部非React的静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics';
export default (title = '默认标题') => (WrappedComponent) => {
  class HOC extends Component {
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
    render() {
      return (
        <fieldset>
          <legend>{title}</legend>
          <WrappedComponent {...this.props} />
        </fieldset>
      );
    }
  }
  // 拷贝静态方法
  hoistNonReactStatic(HOC, WrappedComponent);
  return HOC;
};

Refs属性不能传递

通常来讲,高阶组件能够传递全部的props属性给包裹的组件,可是不能传递 refs 引用。由于并非像 key 同样,refs 是一个伪属性,React 对它进行了特殊处理。
若是你向一个由高级组件建立的组件的元素添加 ref 应用,那么 ref 指向的是最外层容器组件实例的,而不是包裹组件。
但有的时候,咱们不可避免要使用 refs,官方给出的解决方案是:

传递一个ref回调函数属性,也就是给ref应用一个不一样的名字

const Hello = createReactClass({
  componentDidMount: function() {
    var component = this.hello;
    // ...do something with component
  },
  render() {
    return <div ref={(c) => { this.hello = c; }}>Hello, world.</div>;
  }
});

反向继承不能保证完整的子组件树被解析

这里要注意的是:

  • 元素(element)是一个是用DOM节点或者组件来描述屏幕显示的纯对象,元素能够在属性(props.children)中包含其余的元素,一旦建立就不会改变。咱们经过JSXReact.createClass建立的都是元素。
  • 组件(component)能够接受属性(props)做为输入,而后返回一个元素树(element tree)做为输出。有多种实现方式:Class或者函数(Function)。

反向继承不能保证完整的子组件树被解析的意思的解析的元素树中包含了组件(函数类型或者Class类型),就不能再操做组件的子组件了,这就是所谓的不能彻底解析。详细介绍可参考官方博客

约定

官方文档中也对使用高阶组件有一些约定

将不相关的props属性传递给包裹组件

高阶组件给组件添加新特性。他们不该该大幅修改原组件的接口(译者注:应该就是props属性)。预期,从高阶组件返回的组件应该与原包裹的组件具备相似的接口。

高阶组件应该传递与它要实现的功能点无关的props属性。大多数高阶组件都包含一个以下的render函数:

render() {
  // 过滤掉与高阶函数功能相关的props属性,
  // 再也不传递
  const { extraProp, ...passThroughProps } = this.props;

  // 向包裹组件注入props属性,通常都是高阶组件的state状态
  // 或实例方法
  const injectedProp = someStateOrInstanceMethod;

  // 向包裹组件传递props属性
  return (
    <WrappedComponent
      injectedProp={injectedProp}
      {...passThroughProps}
    />
  );
}

最大化使用组合

并非全部的高阶组件看起来都是同样的。有时,它们仅仅接收一个参数,即包裹组件:

const NavbarWithRouter = withRouter(Navbar);

通常而言,高阶组件会接收额外的参数。在下面这个来自Relay的示例中,可配置对象用于指定组件的数据依赖关系:

const CommentWithRelay = Relay.createContainer(Comment, config);

大部分常见高阶组件的函数签名以下所示:

// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(Comment);

这是什么?! 若是你把它剥开,你就很容易看明白究竟是怎么回事了。

// connect是一个返回函数的函数(译者注:就是个高阶函数)
const enhance = connect(commentListSelector, commentListActions);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store
// 关联起来的新组件
const ConnectedComment = enhance(CommentList);

换句话说,connect 是一个返回高阶组件的高阶函数!

这种形式有点让人迷惑,有点多余,可是它有一个有用的属性。那就是,相似 connect 函数返回的单参数的高阶组件有着这样的签名格式, Component => Component.输入和输出类型相同的函数是很容易组合在一块儿。

// 不要这样作……
const EnhancedComponent = connect(commentSelector)(withRouter(WrappedComponent))

// ……你可使用一个功能组合工具
// compose(f, g, h) 和 (...args) => f(g(h(...args)))是同样的
const enhance = compose(
  // 这些都是单参数的高阶组件
  connect(commentSelector),
  withRouter
)
const EnhancedComponent = enhance(WrappedComponent)

connect函数产生的高阶组件和其它加强型高阶组件具备一样的被用做装饰器的能力。)

包括lodash(好比说lodash.flowRight), ReduxRamda在内的许多第三方库都提供了相似compose功能的函数。

包装显示名字以便于调试

高价组件建立的容器组件在React Developer Tools中的表现和其它的普通组件是同样的。为了便于调试,能够选择一个好的名字,确保可以识别出它是由高阶组件建立的新组件仍是普通的组件。

最经常使用的技术就是将包裹组件的名字包装在显示名字中。因此,若是你的高阶组件名字是 withSubscription,且包裹组件的显示名字是 CommentList,那么就是用 withSubscription(CommentList)这样的显示名字:

function withSubscription(WrappedComponent) {
  class WithSubscription extends React.Component {/* ... */}
  WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
  return WithSubscription;
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

参考文档

  1. 深刻理解 React 高阶组件
  2. Higher-Order Components
  3. React 高阶组件浅析
  4. React 高阶组件(HOC)入门指南
  5. React进阶——使用高阶组件(Higher-order Components)优化你的代码
相关文章
相关标签/搜索