HOC 实践

高阶组件(Higher Order Component) 不属于 React API 范畴, 但是它在 React 中也是一种实用的技术, 它可以将常见任务抽象成一个可重用的部分。这个小节算是番外篇, 会结合 cpreact(前文实现的类 react 轮子) 与 HOC 进行相关的实践。

它可以用如下公式表示:

y = f(x),

// x: 原有组件
// y: 高阶组件
// f():

f() 的实现有两种方法, 下面进行实践。

属性代理(Props Proxy)

属性代理是装饰器模式的一种运用, 通过装饰器函数给原来函数赋能。下面例子在装饰器函数中给被装饰的组件传递了额外的属性 { a: 1, b: 2 }

声明: 下文所展示的 demo 均已在 cpreact 测试通过

function ppHOC(WrappedComponent) {
  return class extends Component {
    render() {
      const obj = { a: 1, b: 2 }
      return (
        <WrappedComponent { ...this.props } { ...obj } />
      )
    }
  }
}

@ppHOC
class B extends Component {
  render() {
    return (
      <div>
        { this.props.a + this.props.b } { /* 输出 3 */ }
      </div>
    )
  }
}

要是将 { a: 1, b: 2 } 替换成全局共享对象, 那么不就是 react-redux 中的 Connect 了么?

改进上述 demo, 我们就可以实现可插拔的受控组件, 代码示意如下:

function ppDecorate(WrappedComponent) {
  return class extends Component {
    constructor() {
      super()
      this.state = {
        value: ''
      }
      this.onChange = this.onChange.bind(this)
    }

    onChange(e) {
      this.setState({
        value: e.target.value
      })
    }

    render() {
      const obj = {
        onChange: this.onChange,
        value: this.state.value,
      }

      return (
        <WrappedComponent { ...this.props } { ...obj } />
      )
    }
  }
}

@ppDecorate
class B extends Component {
  render() {
    return (
      <div>
        <input { ...this.props } />
        <div>{ this.props.value }</div>
      </div>
    )
  }
}

效果如下图:

这里有个坑点, 当我们在输入框输入字符的时候, 并不会立马触发 onChange 事件(我们想要让事件立即触发, 然而现在要按下回车键或者点下鼠标才触发), 在 react 中有个合成事件 的知识点, 下篇文章 onChange 事件 对 react 中的 onChange 事件为何和原生 DOM 事件中的 onchange 表现不一致进行揭秘。

顺带一提在这个 demo 中似乎看到了双向绑定的效果, 但是实际中 React 并没有双向绑定的概念, 但是我们可以运用 HOC 的知识点结合 setState 在 React 表单中实现伪双向绑定的效果。

继承反转(Inheritance Inversion)

继承反转的核心是: 传入 HOC 的组件会作为返回类的父类来使用。然后在 render 中调用 super.render() 来调用父类的 render 方法。

ES6 继承与 ES5 继承的差异中提到了作为对象使用的 super 指向父类的实例。

function iiHOC(WrappedComponent) {
  return class extends WrappedComponent {
    render() {
      const parentRender = super.render()
      if (parentRender.nodeName === 'span') {
        return (
          <span>继承反转</span>
        )
      }
    }
  }
}

@iiHOC
class B extends Component {
  render() {
    return (
      <span>Inheritance Inversion</span>
    )
  }
}

在这个 demo 中, 在 HOC 内实现了渲染劫持, 页面上最终显示如下:

可能会有疑惑, 使用属性代理的方式貌似也能实现渲染劫持呀, 但是那样做没有继承反转这种方式纯粹。

相关链接