美文网首页web前端高级-react-redux
组件复用之高阶组件(Higher Order Component

组件复用之高阶组件(Higher Order Component

作者: 老鼠AI大米_Java全栈 | 来源:发表于2020-01-14 17:18 被阅读0次

    高阶组件(HOC)是一个函数返回一个React组件,指的就是一个React组包裹着另一个React组件。可以理解为一个生产React组件的工厂。

    什么是高阶组件?

    高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。
    高阶组件是一个函数(而不是组件),它接受一个组件作为参数,返回一个新的组件。这个新的组件会使用你传给它的组件作为子组件。

    常用高阶函数

    • Props Proxy(pp) HOC对被包裹组件WrappedComponent的props进行操作。
    • Inherbitance Inversion(ii)HOC继承被包裹组件WrappedComponent。
      注意,第二种方式可能导致子组件不完全解析。

    Props Proxy方式

    一种最简单的Props Proxy实现

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

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

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

    这里是将应该传给List组件的属性name, type等,都传给了它的返回值ListHOCInstance,这样的就相当于在List外面加了一层代理,这个代理用于处理即将传给WrappedComponent的props,这也是这种HOC为什么叫Props Proxy

    在pp组件中,我们可以对WrappedComponent进行以下操作:

    • 操作props(增删改)
    • 通过refs访问到组件实例
    • 提取state
    • 用其他元素包裹WrappedComponent

    增加props

    添加新的props给WrappedComponent:

    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

    通过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 ,调用实例的方法。

    不过这里有个问题,如果WrappedComponent是个无状态组件,则在proc中的wrappedComponentInstance是null,因为无状态组件没有this,不支持ref, 这就需要把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);
    

    高阶组件应用--localStorage

    import React, { Component } from 'react'
    
    export default (WrappedComponent, name) => {
      class NewComponent extends Component {
        constructor () {
          super()
          this.state = { data: null }
        }
    
        componentWillMount () {
          let data = localStorage.getItem(name)
          this.setState({ data })
        }
    
        render () {
          return <WrappedComponent data={this.state.data} />
        }
      }
      return NewComponent
    }
    

    现在 NewComponent 会根据第二个参数 name 在挂载阶段从 LocalStorage 加载数据,并且 setState 到自己的 state.data 中,而渲染的时候将 state.data 通过 props.data 传给 WrappedComponent。

    这个高阶组件有什么用呢?假设上面的代码是在 src/wrapWithLoadData.js 文件中的,我们可以在别的地方这么用它:

    import wrapWithLoadData from './wrapWithLoadData'
    
    class InputWithUserName extends Component {
      render () {
        return <input value={this.props.data} />
      }
    }
    
    InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
    export default InputWithUserName
    

    假如 InputWithUserName 的功能需求是挂载的时候从 LocalStorage 里面加载 username 字段作为 <input /> 的 value 值,现在有了 wrapWithLoadData,我们可以很容易地做到这件事情。

    只需要定义一个非常简单的 InputWithUserName,它会把 props.data 作为 <input /> 的 value 值。然把这个组件和 'username' 传给 wrapWithLoadData,wrapWithLoadData 会返回一个新的组件,我们用这个新的组件覆盖原来的 InputWithUserName,然后再导出去模块。

    别人用这个组件的时候实际是用了被加工过的组件:

    import InputWithUserName from './InputWithUserName'
    
    class Index extends Component {
      render () {
        return (
          <div>
            用户名:<InputWithUserName />
          </div>
        )
      }
    }
    

    根据 wrapWithLoadData 的代码我们可以知道,这个新的组件挂载的时候会先去 LocalStorage 加载数据,渲染的时候再通过 props.data 传给真正的 InputWithUserName。

    如果现在我们需要另外一个文本输入框组件,它也需要 LocalStorage 加载 'content' 字段的数据。我们只需要定义一个新的 TextareaWithContent:

    import wrapWithLoadData from './wrapWithLoadData'
    
    class TextareaWithContent extends Component {
      render () {
        return <textarea value={this.props.data} />
      }
    }
    
    TextareaWithContent = wrapWithLoadData(TextareaWithContent, 'content')
    export default TextareaWithContent
    

    写起来非常轻松,我们根本不需要重复写从 LocalStorage 加载数据字段的逻辑,直接用 wrapWithLoadData 包装一下就可以了。
    我们来回顾一下到底发生了什么事情,对于 InputWithUserName 和 TextareaWithContent 这两个组件来说,它们的需求有着这么一个相同的逻辑:“挂载阶段从 LocalStorage 中加载特定字段数据”。

    如果按照之前的做法,我们需要给它们两个都加上 componentWillMount 生命周期,然后在里面调用 LocalStorage。要是有第三个组件也有这样的加载逻辑,我又得写一遍这样的逻辑。但有了 wrapWithLoadData 高阶组件,我们把这样的逻辑用一个组件包裹了起来,并且通过给高阶组件传入 name 来达到不同字段的数据加载。充分复用了逻辑代码。

    高阶组件的灵活性

    代码复用的方法、形式有很多种,你可以用类继承来做到代码复用,也可以分离模块的方式。但是高阶组件这种方式很有意思,也很灵活。学过设计模式的同学其实应该能反应过来,它其实就是设计模式里面的装饰者模式。它通过组合的方式达到很高的灵活程度。

    假设现在我们需求变化了,现在要的是通过 Ajax 加载数据而不是从 LocalStorage 加载数据。我们只需要新建一个 wrapWithAjaxData 高阶组件:

    import React, { Component } from 'react'
    
    export default (WrappedComponent, name) => {
      class NewComponent extends Component {
        constructor () {
          super()
          this.state = { data: null }
        }
    
        componentWillMount () {
          ajax.get('/data/' + name, (data) => {
            this.setState({ data })
          })
        }
    
        render () {
          return <WrappedComponent data={this.state.data} />
        }
      }
      return NewComponent
    }
    

    其实就是改了一下 wrapWithLoadData 的 componentWillMount 中的逻辑,改成了从服务器加载数据。现在只需要把 InputWithUserName 稍微改一下:

    import wrapWithAjaxData from './wrapWithAjaxData'
    
    class InputWithUserName extends Component {
      render () {
        return <input value={this.props.data} />
      }
    }
    
    InputWithUserName = wrapWithAjaxData(InputWithUserName, 'username')
    export default InputWithUserName
    

    只要改一下包装的高阶组件就可以达到需要的效果。而且我们并没有改动 InputWithUserName 组件内部的任何逻辑,也没有改动 Index 的任何逻辑,只是改动了中间的高阶组件函数。

    多层高阶组件

    假如现在需求有变化了:我们需要先从 LocalStorage 中加载数据,再用这个数据去服务器取数据。我们改一下(或者新建一个)wrapWithAjaxData 高阶组件,修改其中的 componentWillMount:

    componentWillMount () {
          ajax.get('/data/' + this.props.data, (data) => {
            this.setState({ data })
          })
        }
    

    它会用传进来的 props.data 去服务器取数据。这时候修改 InputWithUserName:

    import wrapWithLoadData from './wrapWithLoadData'
    import wrapWithAjaxData from './wrapWithAjaxData'
    
    class InputWithUserName extends Component {
      render () {
        return <input value={this.props.data} />
      }
    }
    
    InputWithUserName = wrapWithAjaxData(InputWithUserName)
    InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
    export default InputWithUserName
    

    可以看到,我们给 InputWithUserName 应用了两种高阶组件:先用 wrapWithAjaxData 包裹 InputWithUserName,再用 wrapWithLoadData 包含上次包裹的结果。它们的关系就如下图的三个圆圈:


    image.png

    实际上最终得到的组件会先去 LocalStorage 取数据,然后通过 props.data 传给下一层组件,下一层用这个 props.data 通过 Ajax 去服务端取数据,然后再通过 props.data 把数据传给下一层,也就是 InputWithUserName。大家可以体会一下下图尖头代表的组件之间的数据流向:


    image2.png

    总结

    高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。新的组件使用传入的组件作为子组件。

    高阶组件的作用是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。新的组件和传入的组件通过 props 传递信息。

    高阶组件有助于提高我们代码的灵活性,逻辑的复用性。

    参考:
    https://segmentfault.com/a/1190000012232867
    https://www.cnblogs.com/hanmeimei/p/8806340.html

    相关文章

      网友评论

        本文标题:组件复用之高阶组件(Higher Order Component

        本文链接:https://www.haomeiwen.com/subject/hghaactx.html