愚墨的博客
  • 首页
  • 前端技术
  • 面试
只争朝夕不负韶华
  1. 首页
  2. 前端框架
  3. React
  4. 正文

React高阶组件应用

2017年03月15日 2952点热度 0人点赞 0条评论

higher-order 这个词业界老司机基本上都知道,higher-order-function(高阶函数)在函数式编程中是一种概念。描述的是一种函数:这种函数接受函数作为参数,或者输出一个参数。 常见的有map、sort、reduce

higher-order-component (高阶组件HOC)类似于高阶函数,它接受一个React组件作为参数,返回一个新的React组件。

通俗点讲就是:当React组件呗包裹时(wrapped),高阶组件会返回一个加强的React组件。  高阶组件让我们的代码更具有复用性、逻辑性和抽象特征。它可以对render方法作劫持,也可以控制props和state

实现高阶组件有两种方式:

  • 属性代理(props proxy)。高阶组件通过被包裹的React组件来操作props
  • 反向继承(inheritance inversion)。 高阶组件继承于被包裹的React组件。

1、属性代理

属性代理是最常见的高阶组件的实现方式:

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

最重要的部分是render中返回来传入的WrappedComponent 的React组件。这样我们可以通过高阶组件来传递、修改props,这种方法即为属性代理。

当我们使用MyContainer这个高阶组件就会变得很容易:

class MyComponent extends React.Component{
  ....
}

export default MyContainer(MyComponent);

这样组件就可以一层层的作为参数被调用,原始组价就具备了高阶组件对它的修饰。保持了单个组件封装性的同时还保留了易用性。当然我们也可以通过decorator 来转换:

decorator是ES7的新特性,这里不多说,有兴趣可以看一下这里

import React,{Component} from 'React';
@MyContainer
class MyComponent extends Component{
  render (){}
}
export default MyComponent;

简单的替换为作用在类上的decorator,既接受需要装饰的类为参数,返回一个新的内部类。这与高阶组件的定义完全一致。因此,可以认为作用在类上的decorator语法糖简化了高阶组件的调用。

从功能上来讲,高阶组件能做到控制props、通过refs使用引用、抽象state、使用其他元素包裹wrappedComponent。

  • 控制props
    你可以读取、添加、编辑、删除传给 WrappedComponent 的 props。但是要小心操作重要的props,尽可能对高阶组件的props作新的命名以防止混淆

    class InBox extends React.Component {
      render() {
        return (
          <div>this.is {this.props.a}+{this.props.b}</div>
        )
      }
    }
    function HOC(InBox) {
      return class box extends React.Component {
        render() {
          let props = {
            ...this.props,
            a: 'aaa',
            b: 'bbb',
          }
          return (
            <InBox {...props}/>
          )
        }
      }
    }
    export default HOC(InBox)
    
    //->render 结果: this.is aaa+bbb
  • 通过refs使用引用
    在高阶组件中,我们可以接受refs使用WrappedComponent的组件。
    你可以通过引用(ref)访问到 this (WrappedComponent 的实例),但为了得到引用,WrappedComponent 还需要一个初始渲染,意味着你需要在 HOC 的 render 方法中返回 WrappedComponent 元素,让 React 开始它的一致化处理,你就可以得到 WrappedComponent 的实例的引用。
class In extends Component {
  handleClick(){
    console.log(this)
  }
  render() {
    return (
      <button onClick={::this.handleClick}>click</button>
    )
  }
}
function refsHOC(WrappedComponent) {
  return class RefsHOC extends React.Component {
    proc(wrappedComponentInstance) {
      console.log(wrappedComponentInstance)
      // wrappedComponentInstance.method()
    }
    render() {
      const props = Object.assign({}, this.props, {ref: this.proc.bind(this)})
      return <WrappedComponent {...props}/>
    }
  }
}

export default refsHOC(In)

 

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

  • 抽象state
    我们可以通过传入 props 和回调函数把 state 提取出来
    抽象一个input组件来说明

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

    可以这样使用

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

    这样我们就得到一个受控input组件。

  • 使用其他元素包裹wrappedComponent这么做是为了布局或者样式处理等目的,我们可以在wrappedComponent 外包裹其他元素或者组件
    function ppHOC(WrappedComponent) {
      return class PP extends React.Component {
        render() {
          return (
            <div style={{display: 'block'}}>
              <WrappedComponent {...this.props}/>
            </div>
          )
        }
      }
    }

2、反向继承

另一种实现高阶组件的方式成为反向继承。先看一个简单的实现:

function MyContainer(WrappedComponent) {
  return class MyComponent extends WrappedComponent {
    render() {
      return super.render()
    }
  }
}

高阶组件返回的组件继承于wrappedComponent,因为被动的继承了WrappedComponent,所以所有的调用都会反向。这也是这种方法的由来。

在反向继承中高阶组价可以通过this来访问到wrappedComponent,意味着它可以访问到 state、props、组件生命周期方法和 render 方法。但是它不能保证完整的子组件树被解析。

反向继承可以做什么?

  • 渲染劫持(Render Highjacking)
    之所以被称为渲染劫持是因为 HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。
    通过渲染劫持你可以:

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

就像刚才说的,反向继承不能保证完整的子组件呗解析,这以为这将限制渲染劫持的功能。使用渲染劫持你可以完全操作 WrappedComponent 的 render 方法返回的元素树。但是如果元素树包括一个函数类型的 React 组件,你就不能操作它的子组件了。(被 React 的一致化处理推迟到了真正渲染到屏幕时)

例1:条件渲染。当 this.props.loggedIn 为 true 时,这个 HOC 会完全渲染 WrappedComponent 的渲染结果。(假设 HOC 接收到了 loggedIn 这个 prop)

function MyContainer(WrappedComponent) {
  return class MyComponent extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render()
      } else {
        return null
      }
    }
  }
}

例2:修改由 render 方法输出的 React 组件树。

function MyContainer(WrappedComponent) {
  return class MyComponent 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。这也正是样式处理库 Radium 所用的方法

  • 操作 state

HOC 可以读取、编辑和删除 WrappedComponent 实例的 state,如果你需要,你也可以给它添加更多的 state。记住,这会搞乱 WrappedComponent 的 state,导致你可能会破坏某些东西。要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 state 混在一起。

export function MyContainer(WrappedComponent) {
  return class MyComponent 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>
      )
    }
  }
}

这里 HOC 用其他元素包裹着 WrappedComponent,还输出了 WrappedComponent 实例的 props 和 state

3、组件命名

当包裹了一个高阶组件时,我们失去了原始的wrappedComponent 的displayName,而组件名字方便我们开发和调试的重要属性。

通常会用 WrappedComponent 的名字加上一些 前缀作为 HOC 的名字。下面的代码来自 React-Redux:

HOC.displayName = `HOC({getDisplayName(WrappedComponent)})`

//或

class HOC extends ... {
  static displayName = `HOC({getDisplayName(WrappedComponent)})`
  ...
}

getDisplayName 函数:

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName ||
         WrappedComponent.name ||
         ‘Component’
}

我们可以使用recompose库,它已经帮我们实现了相应的方法。

4、组件参数

有的时候我们调用高级组件的时候组要传入一些参数。我们可以这么做:

例子:Props Proxy 模式 的 HOC 最简参数使用方法。关键在于 HOCFactoryFactory 函数。

function HOCFactoryFactory(...params){
  // do something with params
  return function HOCFactory(WrappedComponent) {
    return class HOC extends React.Component {
      render() {
        return <WrappedComponent {...this.props}/>
      }
    }
  }
}

你可以这样用:

HOCFactoryFactory(params)(WrappedComponent)
//或
@HOCFatoryFactory(params)
class WrappedComponent extends React.Component{}

这是利用了函数式编程了特性。

 

参考阅读:https://zhuanlan.zhihu.com/p/24776678#!

书籍:《深入React技术栈》

标签: 暂无
最后更新:2017年09月11日

愚墨

保持饥渴的专注,追求最佳的品质

点赞
< 上一篇
下一篇 >

文章评论

取消回复

搜搜看看
历史遗迹
  • 2023年5月
  • 2022年9月
  • 2022年3月
  • 2022年2月
  • 2021年12月
  • 2021年8月
  • 2021年7月
  • 2021年5月
  • 2021年4月
  • 2021年2月
  • 2021年1月
  • 2020年12月
  • 2020年11月
  • 2020年9月
  • 2020年7月
  • 2020年5月
  • 2020年4月
  • 2020年3月
  • 2020年1月
  • 2019年5月
  • 2019年3月
  • 2019年2月
  • 2019年1月
  • 2018年9月
  • 2018年3月
  • 2018年2月
  • 2018年1月
  • 2017年11月
  • 2017年7月
  • 2017年6月
  • 2017年3月
  • 2017年2月
  • 2017年1月
  • 2016年12月
  • 2016年11月
  • 2016年9月
  • 2016年8月
  • 2016年7月
  • 2016年6月
  • 2016年5月
  • 2016年4月
  • 2016年3月
  • 2016年2月
  • 2016年1月
  • 2015年12月
  • 2015年10月
  • 2015年9月
  • 2015年7月
  • 2015年6月
  • 2015年4月

COPYRIGHT © 2020 愚墨的博客. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS