Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 高阶组件浅析 #44

Open
SunShinewyf opened this issue May 19, 2018 · 0 comments
Open

React 高阶组件浅析 #44

SunShinewyf opened this issue May 19, 2018 · 0 comments

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented May 19, 2018

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

在谈及高阶组件之前,我们先来讲讲它的前身 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,后续会持续踩坑~

参考文章

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant