前言

最近在学习React的封装,虽然日常的开发中也有用到HOC或者Render Props,但从继承到组合,静态构建到动态渲染,都是似懂非懂,索性花时间系统性的整理,如有错误,请轻喷~~

例子

以下是React官方的一个例子,我会采用不同的封装方法来尝试代码复用,例子地址

组件在 React 是主要的代码复用单元,但如何共享状态或一个组件的行为封装到其他需要相同状态的组件中并不是很明了
例如,下面的组件在 web 应用追踪鼠标位置:

class MouseTracker extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <h1>Move the mouse around!</h1>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

随着鼠标在屏幕上移动,在一个

的组件上显示它的 (x, y) 坐标。

现在的问题是:我们如何在另一个组件中重用行为?换句话说,若另一组件需要知道鼠标位置,我们能否封装这一行为以让能够容易在组件间共享?

由于组件是 React 中最基础的代码重用单元,现在尝试重构一部分代码能够在 组件中封装我们需要在其他地方的行为。

// The <Mouse> component encapsulates the behavior we need...
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/* ...but how do we render something other than a <p>? */}
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse />
      </div>
    );
  }
}

现在 组件封装了所有关于监听 mousemove 事件和存储鼠标 (x, y) 位置的行为,但其仍不失真正的可重用。

例如,假设我们现在有一个在屏幕上跟随鼠标渲染一张猫的图片的 组件。我们可能使用 <Cat mouse={{ x, y }} prop 来告诉组件鼠标的坐标以让它知道图片应该在屏幕哪个位置。

首先,你可能会像这样,尝试在 的内部的渲染方法 渲染 组件:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class MouseWithCat extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          We could just swap out the <p> for a <Cat> here ... but then
          we would need to create a separate <MouseWithSomethingElse>
          component every time we need to use it, so <MouseWithCat>
          isn't really reusable yet.
        */}
        <Cat mouse={this.state} />
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <MouseWithCat />
      </div>
    );
  }
}

这一方法对我们的具体用例来说能够生效,但我们却没法实现真正的将行为封装成可重用的方式的目标。现在,每次我们在不同的用例中想要使用鼠标的位置,我们就不得不创建一个新的针对那一用例渲染不同内容的组件 (如另一个关键的 <MouseWithCat>)

Mixin

Mixin概念

React Mixin将通用共享的方法包装成Mixins方法,然后注入各个组件实现,事实上已经是不被官方推荐使用了,但仍然可以学习一下,了解其为什么被遗弃,先从API看起。
React Mixin只能通过React.createClass()使用, 如下:

var mixinDefaultProps = {}
var ExampleComponent = React.createClass({
    mixins: [mixinDefaultProps],
    render: function(){}
});

Mixin实现

// 封装的Mixin
const mouseMixin = {
  getInitialState() {
    return {
      x: 0,
      y: 0
    }
  },
  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    })
  }
}

const Mouse = createReactClass({
  mixins: [mouseMixin],
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
      </div>
    )
  }
})

const Cat = createReactClass({
  mixins: [mouseMixin],
  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <img src="/cat.jpg" style={{ position: 'absolute', left: this.state.x, top: this.state.y }} alt="" />
      </div>
    )
  }
})

Mixin的问题

然而,为什么Mixin会被不推荐使用?归纳起来就是以下三点

1. Mixin引入了隐式依赖关系 如:

3. Mixin导致复杂的滚雪球

4. 拥抱ES6,ES6的class不支持Mixin

HOC

HOC概念

高阶组件(HOC)是react中的高级技术,用来重用组件逻辑。但高阶组件本身并不是React API。它只是一种模式,这种模式是由react自身的组合性质必然产生的,是React社区发展中产生的一种模式
高阶组件的名称是从高阶函数来的, 如果了解过函数式编程, 就会知道高阶函数就是一个入参是函数,返回也是函数的函数,那么高阶组件顾名思义,就是一个入参是组件,返回也是组件的函数,如:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

HOC实现

高阶组件在社区中, 有两种使用方式, 分别是:

  • Props Proxy: HOC 对传给 WrappedComponent W 的 porps 进行操作。
  • Inheritance Inversion: HOC 继承 WrappedComponent W。

依然是使用之前的例子, 先从比较普通使用的Props Proxy看起:

class Mouse extends React.Component {
  render() {
    const { x, y } = this.props.mouse
    return (
      <p>The current mouse position is ({x}, {y})</p>
    )
  }
}

class Cat extends React.Component {
  render() {
    const { x, y } = this.props.mouse
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: x, top: y }} alt="" />
    )
  }
}


const MouseHoc = (MouseComponent) => {
  return class extends React.Component {
    constructor(props) {
      super(props)
      this.handleMouseMove = this.handleMouseMove.bind(this)
      this.state = { x: 0, y: 0 }
    }

    handleMouseMove(event) {
      this.setState({
        x: event.clientX,
        y: event.clientY
      })
    }
    render() {
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
          <MouseComponent mouse={this.state} />
        </div>
      )
    }
  }
}

const EnhanceMouse = MouseHoc(Mouse)
const EnhanceCat = MouseHoc(Cat)

那么在Hoc的Props Proxy模式下, 我们可以做什么?

操作Props
如上面的MouseHoc, 假设在日常开发中,我们需要传入一个props给Mouse或者Cat,那么我们可以在HOC里面对props进行增删查改等操作,如下:

const MouseHoc = (MouseComponent, props) => {
  props.text = props.text + '---I can operate props'
  return class extends React.Component {
    ......
    render() {
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
          <MouseComponent {...props} mouse={this.state} />
        </div>
      )
    }
  }
}
MouseHoc(Mouse, {
  text: 'some thing...'
})

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

提取state
就是我们的例子。

<MouseComponent mouse={this.state} />

包裹 WrappedComponent

<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
    <MouseComponent mouse={this.state} />
</div>

另外一种HOC模式则是Inheritance Inversion,不过该模式比较少见,一个最简单的例子如下:

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

那么在我们的例子中它是这样的:

class Mouse extends React.Component {
  render(props) {
    const { x, y } = props.mouse
    return (
      <p>The current mouse position is ({x}, {y})</p>
    )
  }
}

class Cat extends React.Component {
  render(props) {
    const { x, y } = props.mouse
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: x, top: y }} alt="" />
    )
  }
}


const MouseHoc = (MouseComponent) => {
  return class extends MouseComponent {
    constructor(props) {
      super(props)
      this.handleMouseMove = this.handleMouseMove.bind(this)
      this.state = { x: 0, y: 0 }
    }

    handleMouseMove(event) {
      this.setState({
        x: event.clientX,
        y: event.clientY
      })
    }
    render() {
      const props = {
        mouse: this.state
      }
      return (
        <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
          {super.render(props)}
        </div>
      )
    }
  }
}

const EnhanceMouse = MouseHoc(Mouse)
const EnhanceCat = MouseHoc(Cat)

同样, 在II模式下,我们能做些什么呢?

渲染劫持
因为render()返回的就是JSX编译后的对象,如下:
React-代码复用(mixin.hoc.render props)-LMLPHP

可以通过手动修改这个tree,来达到一些需求效果,不过这通常不会用到:

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

操作 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>
      )
    }
  }
}

为什么有Class而不去使用继承返回来使用HOC

可能有人看到这里会有疑惑,为什么有Class而不去使用继承返回来使用HOC, 这里推荐知乎的一个比较好的答案

Mixin和HOC的对比

Mixin就像他的名字,他混入了组件中,我们很难去对一个混入了多个Mixin的组件进行管理,好比一个盒子,我们在盒子里面塞入了各种东西(功能),最后肯定是难以理清其中的脉络。
HOC则像是一个装饰器,他是在盒子的外面一层一层的装饰,当我们想要抽取某一层或者增加某一层都非常容易。

HOC的约定

贯穿传递不相关props属性给被包裹的组件
高阶组件应该贯穿传递与它专门关注无关的props属性。

render() {
  // 过滤掉专用于这个阶组件的props属性,
  // 不应该被贯穿传递
  const { extraProp, ...passThroughProps } = this.props;

  // 向被包裹的组件注入props属性,这些一般都是状态值或
  // 实例方法
  const injectedProp = someStateOrInstanceMethod;

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

最大化的组合性

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

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

包装显示名字以便于调试

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

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

HOC的警戒

  • 不要在render方法内使用高阶组件,因为每次高阶组件返回的都是不同的组件,会造成不必要的渲染。
  • 必须将静态方法做拷贝。

HOC带来的问题:

  • 当存在多个HOC时,你不知道Props是从哪里来的。
  • 和Mixin一样, 存在相同名称的props,则存在覆盖问题,而且react并不会报错。
  • JSX层次中多了很多层次(即无用的空组件),不利于调试。
  • HOC属于静态构建,静态构建即是重新生成一个组件,即返回的新组件,不会马上渲染,即新组件中定义的生命周期函数只有新组件被渲染时才会执行。

Render Props

Render Props概念

Render Props应用

可以看下最初的例子在render props中的应用:

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>

        {/*
          Instead of providing a static representation of what <Mouse> renders,
          use the `render` prop to dynamically determine what to render.
        */}
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

render props的优势

  • 不用担心Props是从哪里来的, 它只能从父组件传递过来。
  • 不用担心props的命名问题。
  • render props是动态构建的。

动态构建和静态构建

这里简单的说下动态构建,因为React官方推崇动态组合,然而HOC实际上是一个静态构建,比如,在某个需求下,我们需要根据Mouse中某个字段来决定渲染Cat组件或者Dog组件,使用HOC会是如下:

const MouseHoc = (Component) => {
  return Class extends React.Component {
   render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        {
          isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} />
        }
      </div>
    );
  }
  }
}

可以看到,我们不得不提前静态构建好Cat和Dog组件

假如我们用Render props:

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={(mouse, isCat) => (
          isCat ? <Cat mouse={mouse} /> : <Dog mouse={mouse} />
        )}/>
      </div>
    );
  }
}

很明显,在动态构建的时候,我们具有更多的灵活性,我们可以更好的利用生命周期,相比较HOC,就不得不引入Cat和Dog组件,污染了MouseHoc。

Render Props的缺点

无法使用SCU做优化, 具体参考官方文档

总结

抛开被遗弃的Mixin和尚未稳定的Hooks,目前社区的代码复用方案主要还是HOC和Render Props,个人感觉,如果是多层组合或者需要动态渲染那就选择Render Props,而如果是诸如在每个View都要执行的简单操作,如埋点、title设置等或者是对性能要求比较高如大量表单可以采用HOC。

参考

Function as Child Components Not HOCs
React高阶组件和render props的适用场景有区别吗,还是更多的是个人偏好?
深入理解 React 高阶组件
高阶组件-React
精读《我不再使用高阶组件》
为什么 React 推崇 HOC 和组合的方式,而不是继承的方式来扩展组件?
React 中的 Render Props
使用 Render props 吧!
渲染属性(Render Props)

03-22 16:43