React进阶之高阶组件

前言

本文代码浅显易懂,思想深刻实用。此属于react进阶用法,若是你还不了解react,建议从文档开始看起。javascript

咱们都知道高阶函数是什么, 高阶组件实际上是差很少的用法,只不过传入的参数变成了react组件,并返回一个新的组件.html

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

形如:react

const EnhancedComponent = higherOrderComponent(WrappedComponent);

高阶组件是react应用中很重要的一部分,最大的特色就是重用组件逻辑。它并非由React API定义出来的功能,而是由React的组合特性衍生出来的一种设计模式。若是你用过redux,那你就必定接触太高阶组件,由于react-redux中的connect就是一个高阶组件。git

原文https://github.com/sunyongjian/blog/issues/25
欢迎star
另外本次demo代码都放在 https://github.com/sunyongjian/hoc-demo
clone下来跑一下加深理解es6

引入

先来一个最简单的高阶组件github

import React, { Component } from 'react'; import simpleHoc from './simple-hoc'; class Usual extends Component { render() { console.log(this.props, 'props'); return ( <div> Usual </div> ) } } export default simpleHoc(Usual);
import React, { Component } from 'react'; const simpleHoc = WrappedComponent => { console.log('simpleHoc'); return class extends Component { render() { return <WrappedComponent {...this.props}/> } } } export default simpleHoc;

组件Usual经过simpleHoc的包装,打了一个log... 那么形如simpleHoc就是一个高阶组件了,经过接收一个组件class Usual,并返回一个组件class。 其实咱们能够看到,在这个函数里,咱们能够作不少操做。 并且return的组件一样有本身的生命周期,function,另外,咱们看到也能够把props传给WrappedComponent(被包装的组件)。 高阶组件的定义我都是用箭头函数去写的,若有不适请参照arrow function算法

装饰器模式

高阶组件能够看作是装饰器模式(Decorator Pattern)在React的实现。即容许向一个现有的对象添加新的功能,同时又不改变其结构,属于包装模式(Wrapper Pattern)的一种chrome

ES7中添加了一个decorator的属性,使用@符表示,能够更精简的书写。那上面的例子就能够改为:编程

import React, { Component } from 'react'; import simpleHoc from './simple-hoc'; @simpleHoc export default class Usual extends Component { render() { return ( <div> Usual </div> ) } }

是一样的效果。
固然兼容性是存在问题的,一般都是经过babel去编译的。 babel提供了plugin,高阶组件用的是类装饰器,因此用transform-decorators-legacy babel

两种形式

属性代理

引入里咱们写的最简单的形式,就是属性代理(Props Proxy)的形式。经过hoc包装wrappedComponent,也就是例子中的Usual,原本传给Usual的props,都在hoc中接受到了,也就是props proxy。 由此咱们能够作一些操做

  • 操做props
    最直观的就是接受到props,咱们能够作任何读取,编辑,删除的不少自定义操做。包括hoc中定义的自定义事件,均可以经过props再传下去。

    import React, { Component } from 'react'; const propsProxyHoc = WrappedComponent => class extends Component { handleClick() { console.log('click'); } render() { return (<WrappedComponent {...this.props} handleClick={this.handleClick} />); } }; export default propsProxyHoc;

    而后咱们的Usual组件render的时候, console.log(this.props) 会获得handleClick.

  • refs获取组件实例
    当咱们包装Usual的时候,想获取到它的实例怎么办,能够经过引用(ref),在Usual组件挂载的时候,会执行ref的回调函数,在hoc中取到组件的实例。经过打印,能够看到它的props, state,都是能够取到的。

    import React, { Component } from 'react'; const refHoc = WrappedComponent => class extends Component { componentDidMount() { console.log(this.instanceComponent, 'instanceComponent'); } render() { return (<WrappedComponent {...this.props} ref={instanceComponent => this.instanceComponent = instanceComponent} />); } }; export default refHoc;
  • 抽离state

    这里不是经过ref获取state, 而是经过 { props, 回调函数 } 传递给wrappedComponent组件,经过回调函数获取state。这里用的比较多的就是react处理表单的时候。一般react在处理表单的时候,通常使用的是受控组件(文档),即把input都作成受控的,改变value的时候,用onChange事件同步到state中。固然这种操做经过Container组件也能够作到,具体的区别放到后面去比较。看一下代码就知道怎么回事了:

    // 普通组件Login import React, { Component } from 'react'; import formCreate from './form-create'; @formCreate export default class Login extends Component { render() { return ( <div> <div> <label id="username"> 帐户 </label> <input name="username" {...this.props.getField('username')}/> </div> <div> <label id="password"> 密码 </label> <input name="password" {...this.props.getField('password')}/> </div> <div onClick={this.props.handleSubmit}>提交</div> <div>other content</div> </div> ) } }
    //HOC import React, { Component } from 'react'; const formCreate = WrappedComponent => class extends Component { constructor() { super(); this.state = { fields: {}, } } onChange = key => e => { const { fields } = this.state; fields[key] = e.target.value; this.setState({ fields, }) } handleSubmit = () => { console.log(this.state.fields); } getField = fieldName => { return { onChange: this.onChange(fieldName), } } render() { const props = { ...this.props, handleSubmit: this.handleSubmit, getField: this.getField, } return (<WrappedComponent {...props} />); } }; export default formCreate;

    这里咱们把state,onChange等方法都放到HOC里,实际上是听从的react组件的一种规范,子组件简单,傻瓜,负责展现,逻辑与操做放到Container。好比说咱们在HOC获取到用户名密码以后,再去作其余操做,就方便多了,而state,处理函数放到Form组件里,只会让Form更加笨重,承担了本不属于它的工做,这样咱们可能其余地方也须要用到这个组件,可是处理方式稍微不一样,就很麻烦了。

反向继承

反向继承(Inheritance Inversion),简称II,原本我是叫继承反转的...由于有个模式叫控制反转嘛...
跟属性代理的方式不一样的是,II采用经过 去继承WrappedComponent,原本是一种嵌套的关系,结果II返回的组件却继承了WrappedComponent,这看起来是一种反转的关系。
经过继承WrappedComponent,除了一些静态方法,包括生命周期,state,各类function,咱们均可以获得。上栗子:

// usual import React, { Component } from 'react'; import iiHoc from './ii-hoc'; @iiHoc export default class Usual extends Component { constructor() { super(); this.state = { usual: 'usual', } } componentDidMount() { console.log('didMount') } render() { return ( <div> Usual </div> ) } }
//IIHOC import React from 'react'; const iiHoc = WrappedComponent => class extends WrappedComponent { render() { console.log(this.state, 'state'); return super.render(); } } export default iiHoc;

iiHoc return的组件经过继承,拥有了Usual的生命周期及属性,因此didMount会打印,state也经过constructor执行,获得state.usual。
其实,你还能够经过II:

渲染劫持

这里HOC里定义的组件继承了WrappedComponent的render(渲染),咱们能够以此进行hijack(劫持),也就是控制它的render函数。栗子:

//hijack-hoc import React from 'react'; const hijackRenderHoc = config => WrappedComponent => class extends WrappedComponent { render() { const { style = {} } = config; const elementsTree = super.render(); console.log(elementsTree, 'elementsTree'); if (config.type === 'add-style') { return <div style={{...style}}> {elementsTree} </div>; } return elementsTree; } }; export default hijackRenderHoc;
//usual @hijackRenderHoc({type: 'add-style', style: { color: 'red'}}) class Usual extends Component { ... }

我这里经过二阶函数,把config参数预制进HOC, 算是一种柯理化的思想。
栗子很简单,这个hoc就是添加样式的功能。可是它暴露出来的信息却很多。首先咱们能够经过config参数进行逻辑判断,有条件的渲染,固然这个参数的做用不少,react-redux中的connect不就是传入了props-key 嘛。再就是咱们还能够拿到WrappedComponent的元素树,能够进行修改操做。最后就是咱们经过div包裹,设置了style。但其实具体如何操做仍是根据业务逻辑去处理的...

element-tree

个人应用场景

  • 一般我会经过高阶组件去优化以前老项目写的很差的地方,好比两个页面UI几乎同样,功能几乎相同,仅仅几个操做不太同样,却写了两个耦合不少的页面级组件。当我去维护它的时候,因为它的耦合性过多,常常会添加一个功能(这两个组件都要添加),我要去改完第一个的时候,还要改第二个。并且有时候因为个人记性很差,会忘掉第二个... 就会出现bug再返工。更重要的是因为我的比较懒,不想去重构这部分的代码,由于东西太多了,花费太多时间。因此加新功能的时候,我会写一个高阶组件,往HOC里添加方法,把那两个组件包装一下,也就是属性代理。这样新代码就不会再出现耦合,旧的逻辑并不会改变,说不定哪天心情好就会抽离一部分功能到HOC里,直到理想的状态。
  • 另外一种状况就是以前写过一个组件A,作完上线,以后产品加了一个新需求,很奇怪要作的组件B跟A几乎如出一辙,但稍微有区别。那我可能就经过II的方式去继承以前的组件A,好比它在didMount去fetch请求,须要的数据是同样的。不一样的地方我就会放到HOC里,存储新的state这样,再经过劫持渲染,把不一样的地方,添加的地方进行处理。但其实这算Hack的一种方式,能快速解决问题,也反映了组件设计规划之初有所不足(缘由比较多)。

  • Container解决不了的时候甚至不太优雅的时候。其实大部分时候包一层Container组件也能作到差很少的效果,好比操做props,渲染劫持。但其实仍是有很大区别的。好比咱们如今有两个功能的container,添加样式和添加处理函数的,对Usual进行包装。栗子:

    //usual class Usual extends Component { render() { console.log(this.props, 'props'); return <div> Usual </div> } }; export default Usual; //console - Object {handleClick: function} "props"
    import React, { Component } from 'react'; import Usual from './usual'; class StyleContainer extends Component { render() { return (<div style={{ color: '#76d0a3' }}> <div>container</div> <Usual {...this.props} /> </div>); } } export default StyleContainer;
    import React, { Component } from 'react'; import StyleContainer from './container-add-style'; class FuncContainer extends Component { handleClick() { console.log('click'); } render() { const props = { ...this.props, handleClick: this.handleClick, }; return (<StyleContainer {...props} />); } } export default FuncContainer;

    外层Container必需要引入内层Container,进行包装,还有props的传递,一样要注意包装的顺序。固然你能够把全部的处理都放到一个Container里。那用HOC怎么处理呢,相信你们有清晰的答案了。

    const addFunc = WrappedComponent => class extends Component { handleClick() { console.log('click'); } render() { const props = { ...this.props, handleClick: this.handleClick, }; return <WrappedComponent {...props} />; } };
    const addStyle = WrappedComponent => class extends Component { render() { return (<div style={{ color: '#76d0a3' }}> <WrappedComponent {...this.props} /> </div>); } };
    const WrappenComponent = addStyle(addFunc(Usual)); class WrappedUsual extends Component { render() { console.log(this.props, 'props'); return (<div> <WrappedComponent /> </div>); } }

    显然HOC是更优雅一些的,每一个HOC都定义本身独有的处理逻辑,须要的时候只须要去包装你的组件。相较于Container的方式,HOC耦合性更低,灵活性更高,能够自由组合,更适合应付复杂的业务。固然当你的需求很简单的时候,仍是用Container去自由组合,应用场景须要你清楚。

注意点(约束)

其实官网有不少,简单介绍一下。

  • 最重要的原则就是,注意高阶组件不会修改子组件,也不拷贝子组件的行为。高阶组件只是经过组合的方式将子组件包装在容器组件中,是一个无反作用的纯函数
  • 要给hoc添加class名,便于debugger。我上面的好多栗子组件都没写class 名,请不要学我,由于我实在想不出叫什么名了... 当咱们在chrome里应用React-Developer-Tools的时候,组件结构能够一目了然,因此DisplayName最好仍是加上。
    constructor

  • 静态方法要复制
    不管PP仍是II的方式,WrappedComponent的静态方法都不会复制,若是要用须要咱们单独复制。

  • refs不会传递。 意思就是HOC里指定的ref,并不会传递到子组件,若是你要使用最好写回调函数经过props传下去。

  • 不要在render方法内部使用高阶组件。简单来讲react的差分算法会去比较 NowElement === OldElement, 来决定要不要替换这个elementTree。也就是若是你每次返回的结果都不是一个引用,react觉得发生了变化,去更替这个组件会致使以前组件的状态丢失。

    // HOC不要放到render函数里面 class WrappedUsual extends Component { render() { const WrappenComponent = addStyle(addFunc(Usual)); console.log(this.props, 'props'); return (<div> <WrappedComponent /> </div>); } }
  • 使用compose组合HOC。函数式编程的套路... 例如应用redux中的middleware以加强功能。redux-middleware解析

    const addFuncHOC = ... const addStyleHOC = ...//省略 const compose = (...funcs) => component => { if (funcs.lenght === 0) { return component; } const last = funcs[funcs.length - 1]; return funcs.reduceRight((res, cur) => cur(res), last(component)); }; const WrappedComponent = compose(addFuncHOC, addStyleHOC)(Usual);

    关于注意点,官网有所介绍,再也不赘述。连接

总结

高阶组件最大的好处就是解耦和灵活性,在react的开发中仍是颇有用的。固然这不多是高阶组件的所有用法。掌握了它的一些技巧,还有一些限制,你能够结合你的应用场景,发散思惟,尝试一些不一样的用法。

相关文章
相关标签/搜索