React-代码复用(mixin.hoc.render props)

前言

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

例子

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

组件在 React 是主要的代码复用单元,但如何共享状态或一个组件的行为封装到其余须要相同状态的组件中并非很明了
例如,下面的组件在 web 应用追踪鼠标位置: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>
    );
  }
}

随着鼠标在屏幕上移动,在一个 <p> 的组件上显示它的 (x, y) 坐标。 编程

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

因为组件是 React 中最基础的代码重用单元,如今尝试重构一部分代码可以在<Mouse> 组件中封装咱们须要在其余地方的行为。app

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

如今 <Mouse> 组件封装了全部关于监听 mousemove 事件和存储鼠标 (x, y) 位置的行为,但其仍不失真正的可重用。 ide

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

首先,你可能会像这样,尝试在 <Mouse> 的内部的渲染方法 渲染 <Cat> 组件:函数

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引入了隐式依赖关系 如:

你可能会写一个有状态的组件,而后你的同事可能会添加一个读取这个状态的mixin。在几个月内,您可能须要将该状态移至父组件,以便与兄弟组件共享。你会记得更新mixin来读取道具吗?若是如今其余组件也使用这个mixin呢?

2. Mixin致使名称冲突 如:

你在该Mixin定义了getSomeName, 另一个Mixin又定义了一样的名称getSomeName, 形成了冲突。

3. Mixin致使复杂的滚雪球

随着时间和业务的增加, 你对Mixin的修改愈来愈多, 到最后会变成一个难以维护的Mixin。

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

HOC

HOC概念

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

const EnhancedComponent = higherOrderComponent(WrappedComponent);

HOC实现

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

其中 W (WrappedComponent) 指被包裹的 React.Component,E (EnhancedComponent) 指返回类型为 React.Component 的新的 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()
    }
  }
}
你能够看到,返回的 HOC 类(Enhancer)继承了 WrappedComponent。之因此被称为 Inheritance Inversion 是由于 WrappedComponent 被 Enhancer 继承了,而不是 WrappedComponent 继承了 Enhancer。在这种方式中,它们的关系看上去被反转(inverse)了。Inheritance Inversion 容许 HOC 经过 this 访问到 WrappedComponent,意味着它能够访问到 state、props、组件生命周期方法和 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编译后的对象,以下:
image

能够经过手动修改这个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

HOC 能够读取、编辑和删除 WrappedComponent 实例的 state,若是你须要,你也能够给它添加更多的 state。记住,这会搞乱 WrappedComponent 的 state,致使你可能会破坏某些东西。要限制 HOC 读取或添加 state,添加 state 时应该放在单独的命名空间里,而不是和 WrappedComponent 的 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, 这里推荐知乎的一个比较好的答案

OOP和FP并不矛盾,因此混着用没毛病,不少基于FP思想的库也须要OOP来搭建。
为何React推崇HOC和组合的方式,个人理解是React但愿组件是按照最小可用的思想来进行封装的,理想的说,就是一个组件只作一件的事情,且把它作好,DRY。在OOP原则,这叫单一职责原则。若是要对组件加强,首先应该先思路这个加强的组件须要用到哪些功能,这些功能由哪些组件提供,而后把这些组件组合起来.

image

D中A相关的功能交由D内部的A来负责,D中B相关的功能交由D内部的B来负责,D仅仅负责维护A,B,C的关系,另外也能够额外提供增长项,实现组件的加强。

继承没有什么很差,注意,React只是推荐,但没限制。其实用继承来扩展组件也没问题,并且也存在这样的场景。好比:有一个按钮组件,仅仅是对Button进行一个包装,咱们且叫它Button,但是,按照产品需求,不少地方的按钮都是带着一个icon的,咱们须要提供一个IconButton。这是时候,就能够经过继承来扩展,同时组合另一个独立的组件,咱们且叫它Icon,显示icon的功能交给Icon组件来作,原来按钮的功能继续延续着。对于这种同类型组件的扩展,我认为用继承的方式是不要紧的,灵活性,复用性还在。
可是,用继承的方式扩展前,要先思考,新组件是否与被继承的组件是否是同一类型的,同一类职责的。若是是,能够继承,若是不是,那么就用组合。怎么定义同一类呢,回到上面的Button的例子,所谓同一类,就是说,我直接用IconButton直接替换掉Button,不去改动其余代码,页面依然能够正常渲染,功能能够正常使用,就能够认为是同一类的,在OOP中,这叫作里氏替换原则。

继承会带来什么问题,以个人实践经验,过渡使用继承,虽然给编码带来便利,但容易致使代码失控,组件膨胀,下降组件的复用性。好比:有一个列表组件,叫它ListView吧,能够上下滚动显示一个item集,忽然有一天需求变了,PM说,我要这个ListView能像iOS那样有个回弹效果。好,用继承对这个ListView进行扩展,加入了回弹效果,任务closed。次日PM找上门来了,但愿全部上下滚动的地方均可以支持回弹效果,这时候就懵逼啦,怎么办?把ListView中回弹效果的代码copy一遍?这就和DRY原则相悖了不是,并且有可能受到其余地方代码的影响,处理回弹效果略有不一样,要是有一天PM但愿对这个回弹效果作升级,那就有得改啦。应对这种场景,最好的办法是啥?用组合,封装一个带回弹效果的Scroller,ListView当作是Scroller和item容器组件的组合,其余地方须要要用到滚动的,直接套一个Scroller,之后无论回弹效果怎么变,我只要维护这个Scroller就行了。固然,最理想的,把回弹效果也作成一个组件SpringBackEffect,从Scroller分离出来,这样,须要用回弹效果的地方就加上SpringBackEffect组件就行了,这就是为何组合优先于继承的缘由。

页面简单的时候,组合也好,继承也罢,可维护就好,可以快速的响应需求迭代就好,用什么方式实现到无所谓。但若是是一个大项目,页面用到不少组件,或者是团队多人共同维护的话,就要考虑协做中可能存在的矛盾,而后经过必定约束来闭坑。组合的方式是能够保证组件具备充分的复用性,灵活度,遵照DRY原则的其中一种实践。

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)

包装显示名字以便于调试

最经常使用的技术是包裹显示名字给被包裹的组件。因此,若是你的高阶组件名字是 withSubscription,且被包裹的组件的显示名字是 CommentList,那么就是用 WithSubscription(CommentList)这样的显示名字
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”(属性名也能够不是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 EnhanceCat =  MounseHoc(Cat)
const EnhanceDog =  MounseHoc(Dog)
class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        isCat ? <EnhanceCat /> : <EnhanceDog />
      </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>
    );
  }
}

很明显,在动态构建的时候,咱们具备更多的灵活性,咱们能够更好的利用生命周期

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)

相关文章
相关标签/搜索