SunShinewyf/issue-blog

React 高阶组件浅析

SunShinewyf opened this issue · 0 comments

最近在一些项目中遇到高阶组件的身影,不是很了解,于是深入钻研了一番,以下权当是学习记录了~

在谈及高阶组件之前,我们先来讲讲它的前身 mixin ~
mixin 的作用是:如果多个组件中包含相同的方法(包括普通函数和组件生命周期函数),就可以把这一类函数提取到 mixin 中,然后在需要公共方法的组件中使用 mixin, 就可以避免每个组件都去声明一次,从而达到复用。

React 在早期是使用 createClass 来创建一个 Component 的,而且 createClass 支持 mixin 属性,最常见的就是 react-addons-pure-render-mixin 库提供的 PureRenderMixin 方法,用来减少组件使用中一些不必要的渲染,使用方式如下:

import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
  mixins: [PureRenderMixin],

  render: function() {
    return <div>{this.props.name}</div>;
  }
});

和需要在每一个组件中都重复实现一遍 PureRenderMixin 中浅比较的逻辑相比,上面 mixin 中的使用显得更加简便和明了,同时减少了代码的冗余和重复。

minin 既可以定义多个组件**享的工具方法,同时还可以定义一些组件的生命周期函数(例如上例的 shouldComponentUpdate), 以及初始的 props 和 states。

如下所示:

var propsMixin1 = {
  getDefaultProps: () => {
    return {
      name: "Amy"
    };
  }
};

var propsMixin2 = {
  getDefaultProps: () => {
    return {
      title: "mixin"
    };
  }
};

var MixinExample = createReactClass({
  mixins: [propsMixin1, propsMixin2],
  render: function() {
    return (
      <div>
        <p>{this.props.name}</p>
        <p>{this.props.title}</p>
      </div>
    );
  }
});

但是在使用 mixin 的时候,会有如下的几点需要注意:

  • 不同 mixin 中有相同的函数

    • 组件中使用多个 mixin, 同时不同 mixin 中定义了相同的工具函数,此时会报错(而不是前者覆盖后者)
    • 组件中使用多个 mixin, 同时 mixin 中定义了相同的组件生命周期函数,不会报错,此时会按传给 createClass 的 mixin 数组顺序依次调用,全部调用结束后再调用组件内部的相同的生命周期
  • 不同 mixin 中设置 props 或者 states

    • 组件中含有多个 mixin,不同的 mixin 中默认 props 或初始 state 中存在相同的 key 值时,React 会抛出异常
    • 组件中含有多个 mixin, 不同的 mixin 中默认 props 或初始 state 中存在不同的 key 值时,则默认 props 和初始 state 都会被合并。

附上具体示例代码地址

虽然 mixin 在一定程度上解决了 React 实践中的一些痛点,但是 React 从 v0.13.0 开始,ES6 class 组件写法中不支持 mixins, 但是还是可以使用 createClass 来使用 mixin。之后,React 社区提出了一种新的方式来取代 mixin,那就是高阶组件 Higher-Order Components。

高阶组件

高阶组件 (Higher-Order Components) 是接受一个组件作为参数,然后经过一些处理,返回一个相对增强的组件的函数。它是 React 中的一种模式,而不是 API 的一部分。React 官方给出一个公式描述如下:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

一个最简单的 HOC 例子如下:

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

class Example extends React.PureComponent {
  render() {
    return (
      <div>
        <p>{this.props.age}</p>
      </div>
    );
  }
}

const HocComponent = HOC(Example);
ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

高阶组件的适用场景

它的使用场景有如下几点:

  • 需要抽离可复用的代码逻辑
  • 渲染劫持
  • 更改 state
  • 组装修改 props

高阶组件的实现方式

高阶组件有两种实现方式: 属性代理 (Props Proxy) 和反向继承 (Inheritance Inversion)

属性代理

属性代理是指所有的数据都是从最外层的 HOC 中传给被包裹的组件,它有权限对传入的数据进行修改,对于被包裹组件来说,HOC 对传给自己的属性 (Props) 起到了一层代理作用。

属性代理可以实现如下一些功能:

  • 更改 props
class Example extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    const { name, age, github } = this.props;
    return (
      <div>
        <p>{name}</p>
        <p>{age}</p>
        <p>{github}</p>
      </div>
    );
  }
}

function HOC(WrappedComponent) {
  class EnhancedComponent extends React.PureComponent {
    render() {
      const props = Object.assign({}, this.props, {
        name: "SunShinewyf",
        github: "http://github.com/SunShinewyf"
      });
      return <WrappedComponent {...props} />;
    }
  }
  return EnhancedComponent;
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

如上面的例子中,HOC 对最外层传入的 props 进行了二次组装,扩展了 props 的数据能力。

  • 通过 refs 获取被包裹的组件实例
class Example extends React.PureComponent {
  constructor(props) {
    super(props);
    this.consoleFun.bind(this);
  }

  consoleFun() {
    console.log("hello world");
  }

  render() {
    const { age } = this.props;
    return (
      <div>
        <p>{age}</p>
      </div>
    );
  }
}

function HOC(WrappedComponent) {
  class EnhancedComponent extends React.PureComponent {
    initFunc(instance) {
      instance.consoleFun();
    }
    render() {
      const props = Object.assign({}, this.props, {
        ref: this.initFunc.bind(this)
      });
      return <WrappedComponent {...props} />;
    }
  }
  return EnhancedComponent;
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

如果想要在 HOC 中执行被包裹组件的一些方法,就可以在 props 上组装一下 ref 这个属性,就可以获取到被包裹组件的实例,从而获取到实例的 props 以及它的方法。

  • 组装被包裹组件(WrappedComponent)
function HOC(WrappedComponent) {
  return class PP extends React.Component {
    render() {
      <div>  //添加一些样式
      return <WrappedComponent {...this.props}/>
      </div>
    }
  }
}

这个比较简单,不详述~

反向继承 (Inheritance Inversion)

反向继承是指 HOC 继承被包裹组件,这样被包裹的组件 (WrappedComponent) 就是 HOC 的父组件了,子组件就可以直接操作父组件的所有公开的方法和字段。

反向继承可以实现如下功能:

  • 对 WrappedComponent 的所有生命周期函数进行重写,或者修改其 props 或者 state
class Example extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    console.log("wrappedComponent did mount");
  }

  render() {
    const { age } = this.props;
    return (
      <div>
        <p>{age}</p>
      </div>
    );
  }
}

function HOC(WrapperComponent) {
  return class Inheritance extends WrapperComponent {
    componentDidMount() {
      console.log("HOC did mount");
      super.componentDidMount();
    }
    render() {
      return super.render();
    }
  };
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

// HOC did mount
// wrappedComponent did mount

由上面可以看到,HOC 中定义的生命周期方法可以访问到 WrappedComponent 中的生命周期方法。两者的执行顺序由代码的执行顺序决定。

  • 劫持渲染
class Example extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    const { age } = this.props;
    return <input />;
  }
}

function HOC(WrapperComponent) {
  return class Inheritance extends WrapperComponent {
    render() {
      const elementsTree = super.render();
      let newProps = {};
      if (elementsTree && elementsTree.type === "input") {
        newProps = { defaultValue: "the initialValue of input" };
      }
      const props = Object.assign({}, elementsTree.props, newProps);
      const newElementsTree = React.cloneElement(
        elementsTree,
        props,
        elementsTree.props.children
      );
      return newElementsTree;
    }
  };
}

const HocComponent = HOC(Example);

ReactDom.render(<HocComponent age={24} />, document.getElementById("root"));

运行如上代码,就可以得到一个默认值为 the initialValue of input 的 input 标签。因为 HOC 在 render 之前获取了 WrappedComponent 的 Dom 结构,从而可以自定义一些自己的东西,然后再执行本身的渲染操作。

HOC 的功能虽然很强大,但是在使用过程中还是需要注意,React 官方给出了一些注意事项,在此不赘述~

附上具体示例代码地址

mixin VS HOC

mixin 和 HOC 都能解决代码复用的问题,但是 mixin 存在如下缺点:

  • 降低代码的可读性:组件的优势在于将逻辑与是界面直接结合在一起,mixin 本质上会分散逻辑,理解起来难度大
  • mixin 会导致命名冲突:多个 mixin 和组件本身,方法名称会有命名冲突风险,如果遇到了,不得不重命名某些方法

除了上面的显著缺点外,还有一些其他的,详见 Mixins Considered Harmful

而且 HOC 更接近于函数式编程的**,在使用上也更加灵活,包括的功能点也更多。一张图可以很形象地表达出两者的区别:

images

总结

虽然在 React 实践中,选择实现的方式有很多种,但是为了考虑可维护性和扩展性,还是推荐使用 HOC 的方式。目前暂无很深刻的实践经验,这篇只是纯理论知识+一些简单的 demo,后续会持续踩坑~

参考文章