高阶组件HOC - 小试牛刀

原文地址:https://github.com/SmallStoneSK/Blog/issues/6javascript

1. 前言

老毕曾经有过一句名言,叫做“国庆七天乐,Coding最快乐~”。因此在这漫漫七天长假,手痒了怎么办?因而乎,就有了接下来的内容。。。css

2. 一个中心

今天要分享的内容有关高阶组件的使用。java

虽然这类文章早已经烂大街了,并且想必各位看官也是稔熟于心。所以,本文不会着重介绍一堆HOC的概念,而是经过两个实实在在的实际例子来讲明HOC的用法和强大之处。git

3. 两个例子

3.1 例子1:呼吸动画

首先,咱们来看第一个例子。喏,就是这个。github

呼吸动画

是滴,这个就是呼吸动画(录的动画有点渣,请别在乎。。。),想必你们在绝大多数的APP中都见过这种动画,只不过我这画的很是简陋。在数据ready以前,这种一闪一闪的呼吸动画能够有效地缓解用户的等待心理。web

这时,有人就要跳出来讲了:“这还不简单,建立个控制opacity的animation,再添加class不就行了。。。”是的,在web的世界中,css animation有时真的能够随心所欲。可是我想说,在RN的世界里,只有Animated才真的好使。app

不过话说回来,要用Animated来作这个呼吸动画,的确也很简单。代码以下:dom

class BreathLoading extends React.PureComponent {

  componentWillMount() {
    this._initAnimation();
    this._playAnimation();
  }

  componentWillUnmount() {
    this._stopAnimation();
  }

  _initAnimation() {
    this.oritention = true;
    this.isAnimating = true;
    this.opacity = new Animated.Value(1);
  }

  _playAnimation() {
    Animated.timing(this.opacity, {
      isInteraction: false,
      duration: params.duration,
      toValue: this.oritention ? 0.2 : 1,
      easing: this.oritention ? Easing.in : Easing.easeOut
    }).start(() => {
      this.oritention = !this.oritention;
      this.isAnimating && this._playAnimation();
    });
  }

  _stopAnimation = () => this.isAnimating = false;

  render = () => <Animated.View style={{opacity: this.opacity, width: 100, height: 50, backgroundColor: '#EFEFEF'}}/>;

}

是的,仅二十几行代码咱们就完成了一个简单地呼吸动画。可是问题来了,假如在你的业务需求中有5个、10个场景都须要用到这种呼吸动画怎么办?总不能复制5次、10次,而后修改它们的render方法吧?这也太蠢了。。。函数

有人会想到:“那就封装一个组件呗。反正呼吸动画的逻辑都是不变的,惟一在变的是渲染部分。能够经过props接收一个renderContent方法,将渲染的实际控制权交给调用方。”那就来看看代码吧:fetch

class BreathLoading extends React.PureComponent {
  // ...省略
  render() {
    const {renderContent = () => {}} = this.props;
    return renderContent(this.opacity);
  }
}

相比较于一开始的例子,如今这个BreathLoading组件能够被复用,调用方只要关注本身渲染部分的内容就能够了。可是说实话,我的在这个组件使用方式上总感受有点不舒服,有一个不痛不痒的小问题。习惯上来讲,在真正使用BreathLoading的时候,咱们一般会写出左下图中的这种代码。因为renderContent接收的是一个匿名函数,所以当组件A render的时候,虽然BreathLoading是一个纯组件,可是先后两次接收的renderContent是两个不一样的函数,仍是会发起一次没必要要的domDiff。那还不简单,只要把renderContent中的内容单独抽成一个函数再传进去不就行了(见右下图)。

对溜,这个就是我刚才说的不爽的地方。好端端的一个Loading组件,封装你也封装了,凭啥我还要分两步才能使用。其实BB了那么久,你也知道埋了那么多的铺垫,是时候HOC出场了。。。说来惭愧,在接触HOC以前鄙人一直用的就是上面这种方法来封装。。。直到用上了HOC以后,才发现真香真香。。。

在这里,咱们要用到的是高阶组件的代理模式。你们都知道,高阶组件是一个接收参数、返回组件的函数而已。对于这个呼吸动画的例子而言,咱们来分析一下:

  1. 接收什么?固然是接收刚才renderContent返回的那个组件啦。
  2. 返回什么?固然是返回咱们的BreathLoading组件啦。

OK,看完上面的两句废话以后,再来看下面的代码。

export const WithLoading = (params = {duration: 600}) => WrappedComponent => class extends React.PureComponent {

  componentWillMount() {
    this._initAnimation();
    this._playAnimation();
  }

  componentWillUnmount() {
    this._stopAnimation();
  }

  _initAnimation() {
    this.oritention = true;
    this.isAnimating = true;
    this.opacity = new Animated.Value(1);
  }

  _playAnimation() {
    Animated.timing(this.opacity, {
      isInteraction: false,
      duration: params.duration,
      toValue: this.oritention ? 0.2 : 1,
      easing: this.oritention ? Easing.in : Easing.easeOut
    }).start(() => {
      this.oritention = !this.oritention;
      this.isAnimating && this._playAnimation();
    });
  }

  _stopAnimation = () => this.isAnimating = false;

  render = () => <WrappedComponent opacity={this.opacity} {...this.props}/>;
};

看完上面的代码以后,再回头瞅瞅前面的那两句话,是否是豁然开朗。仔细观察WrappedComponent,咱们发现opacity居然以props的形式传给了它。只要WrappedComponent拿到了关键的opacity,那岂不是想干什么就干什么来着,并且尚未前面说的什么匿名函数和domDiff消耗问题。再配上decorator装饰器,岂不是美滋滋?代码以下:

@WithLoading()
class Test extends React.PureComponent {
  render() {
    const {opacity} = this.props;
    return (
      <View style={{marginTop: 40, paddingHorizontal: 20}}>
        <View style={{marginTop: 20, flexDirection: 'row', justifyContent: 'space-between'}}>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
        </View>
        <View style={{marginTop: 20, flexDirection: 'row', justifyContent: 'space-between'}}>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
          <Animated.View style={{opacity, backgroundColor: '#EFEFEF', width: 150, height: 20}}/>
        </View>
      </View>
    )
  }
}

相比之下,显然高阶组件的用法更胜一筹。之后无论要作成什么样的呼吸动画,只要加一个@withLoading就搞定了。由于这个高阶函数,赋予了普通组件一种呼吸闪烁的能力(记住这句话,圈起来重点考)。

3.2 例子2:多版本控制的组件

通过上面的例子,咱们初步感觉到了高阶组件的黑魔法。由于经过它,咱们能让一个组件拥有某种能力,可以化腐朽为神奇。。。哦,吹过头了。。。那咱们来看第二个例子,也是业务需求中会遇到的场景。为啥?由于善变的产品常常要改版,要作AB!!!

所谓多版本控制的组件,其实就是一个拥有相同功能的组件,因为产品的需求,经历了A版 -> B版 -> C版 -> D版。。。这无穷无尽的改版,有的换个皮肤,改个样式,有的甚至改了交互。

或许对于一个简单的小组件而言,每次改版只要从新建立一个新的组件就能够了。可是,若是对于一个页面级别的Page组件呢?就像下面的这个组件同样,做为容器组件,这个组件充斥着大量复杂的处理逻辑(这里写的是超级简化版的。。。实际应用场景中会复杂的多)。

class X extends Page {

  state = {
    list: []
  };

  componentDidMount() {
    this._fetchData();
  }

  _fetchData = () => setTimeout(() => this.setState({list: [1,2,3]}), 2000);

  onClickHeader = () => console.log('click header');
  
  onClickBody = () => console.log('click body');
  
  onClickFooter = () => console.log('click footer');

  _renderHeader = () => <Header onClick={this.onClickHeader}/>;

  _renderBody = () => <Body data={this.state.list} onClick={this.onClickBody}/>;

  _renderFooter = () => <Footer onClick={this.onClickFooter}/>;

  render = () => (
    <View>
      {this._renderHeader()}
      {this._renderBody()}
      {this._renderFooter()}
    </View>
  );
}

在这种状况下,假如产品要对这个页面作AB该怎么办呢?为了方便作AB,咱们固然但愿建立一个新的Page组件,而后在源头上根据AB实验分别跳转到PageA和PageB便可。可是若是真的copy一份PageA做为PageB,再修改其render方法的话,那请你好好保重。。。要否则怎么办嘞?另外一种很容易想到的办法是在原来Page的render方法中作AB,以下代码:

class X extends Page {

  // ...省略

  _renderHeaderA = () => <HeaderA onClick={this.onClickHeader}/>;

  _renderBodyA = () => <BodyA data={this.state.list} onClick={this.onClickBody}/>;

  _renderFooterA = () => <FooterA onClick={this.onClickFooter}/>;

  _renderHeaderB = () => <HeaderB onClick={this.onClickHeader}/>;

  _renderBodyB = () => <BodyB data={this.state.list} onClick={this.onClickBody}/>;

  _renderFooterB = () => <FooterB onClick={this.onClickFooter}/>;

  render = () => {
    const {version} = this.props;
    return version === 1 ? (
      <View>
        {this._renderHeaderA()}
        {this._renderBodyA()}
        {this._renderFooterA()}
      </View>
    ) : (
      <View>
        {this._renderHeaderB()}
        {this._renderBodyB()}
        {this._renderFooterB()}
      </View>
    );
  }
}

但是这种处理方式有一个很大的弊端!做为Page组件,每每代码量都会比较大,要是再写一堆的renderXXX方法那这个文件势必更加臃肿了。。。要是再改版C、D怎么办?并且很是容易写出诸如version === 1 ? this._renderA() : this._renderB()之类的代码,甚至还有各版本耦合在一块儿的代码,到了后期就更加无法维护了。

那你到底想怎样。。。为了解决上面臃肿的问题,或许咱们能够尝试把这些render方法给移到另外的文件中(这里须要注意两点:因为this问题,咱们须要将Page的实例做为ctx传递下去;为了保证组件可以正常render,须要把state展开传递下去),看下代码:

说实话,这段代码写的足够恶心。。。好好的一个组件被拆得支离破碎,用到this的地方所有被替换成了ctx,还将整个state展开传递下去,看着就很隔应,并且很不习惯,对于新接手的人来讲也容易形成误解。因此这种hack的方式仍是不行,那么到底应该怎么办呢?

噔噔噔噔,高阶组件又要出场了~ 在改造这个Page以前,咱们先来想下,如今这个例子和刚才的呼吸动画那个例子有没有什么类似的地方?答案就是:许多逻辑部分都相同,不一样点在于渲染部分。因此,咱们的重点在于控制render部分,同时还要解决this的指向问题。来看下代码:

重点在两处:一处是constructor的最后一句,咱们将renderEntity中方法都绑定到了Page的实例上;另外一处则是render方法,咱们经过call的方式巧妙地修改了this的指向问题。这样一来,对于PageA和PageB而言,就彻底用不到ctx了。咱们再来对比下原来的Page组件,利用高阶组件,咱们彻底就是将相关的render方法挪了一个位置而已,无形之中还保证了本次修改不会影响到原来的功能。

到了这儿,问题彷佛都迎刃而解,但其实还有一个瑕疵。。。啥?到底有完没完。。。不信,这时候你给PageB中的子组件再加一个onPressXXX事件试试。是哦,这时候事件该加在哪儿呢。。。很简单,有了renderEntity这个先例,再来一个eventEntity不就行了吗。。。看下代码:

真的是不加不知道,一加吓一跳。。。有了eventEntity以后,思路瞬间豁然开朗。由于经过eventEntity,咱们能够将PageA,PageB的事件各自管理,逻辑也被解耦了。咱们能够将各版本Page通用的事件仍然保留在Page中,可是各页面独有的事件写在各自的eventEntity中维护。要是往后再想添加新版本的PageC、PageD,或是废弃PageA,维护管理起来都很是方便。

按照剧情,逼也装够了,其实到这里应该要结束了,但是谁让我又知道了高阶组件的反向继承模式呢。。。前一种的方法惟一的缺点就在于为了hack,咱们无形中将PageA和PageB拆的支离破碎,各类方法散落在Object的各个角落。而反向继承的巧妙之处就在于高阶函数返回的能够是一个继承自传进来的组件的组件,所以对于以前的代码,咱们只要稍加改动便可。看下代码:

相比前一种方法,如今的PageA、PageB显得更加组件了。因此啊,这绕来绕去的,到头来却感受就只迈出了一小步。。。还记得刚才说要圈起来重点考的那句话吗?对于这个多版本组件的例子,咱们只不过是利用高阶组件的形式赋予了PageA,B,C,D这类组件处理该页面业务逻辑的能力。

4. 三点思考

4.1 高阶组件有啥好处?

想必经过上面的两个实际例子,各位看官多多少少已经够体会到高阶组件的好处,由于它确实可以帮助解决平时业务开发中的痛点。其实,高阶组件就是把一些通用的处理逻辑封装在一个高阶函数中,而后返回一个拥有这些逻辑的组件给你。这样一来,你就赋予了一个普通组件某种能力,同时对该组件的入侵也较小。因此啊,若是你的代码中充斥着大量重复性的工做,还不赶忙用起来?

4.2 啥时候用高阶组件?

虽然是建议用高阶组件来解决问题,但可千万别啥都往高阶组件上套。。。实话实说,我还真见过这样的代码。。。可是其实呢,高阶组件自己也只是封装组件的一种方式而已。就比方说文中Loading组件的那个例子,不用高阶不照样能封装一个组件来简化重复性工做吗?

那究竟何时用高阶比较合适呢?还记得先前强调了两遍的那句话么?“高阶组件能够赋予一类组件某种能力” 注意这里的关键词【一类】,在你准备使用高阶组件以前想想,你接下来要作的事情是否是赋予一类组件某种能力?不妨回想一下上面的两个例子,第一个例子是赋予了一类普通组件可以呼吸动画的能力,第二个例子是赋予一类Page组件可以处理当前页面业务逻辑的能力。除此以外,还有一个例子也是特别合适,那就是Animated.createAnimatedComponent,它也是赋予了一类普通组件可以响应Animated.Value变化的能力。因此啊,某种程度上你能够把高阶组件理解为是一种黑魔法,一旦加上了它,你的组件就能拥有某种能力。这个时候,使用高阶组件来封装你的代码再合适不过了。

另外,高阶组件还有一项很是厉害的优点,那就是能够组合。固然了,本文的例子并无体现出这种能力。可是试想,假如你手上有许多个黑魔法(即高阶组件),当你把它们自由组合在一块儿加到某个组件上时,是否是能够创造出无限的可能?而相反,若是你在封装一个组件的时候集成了所有这些功能,这个组件势必会很是臃肿,而当另外的组件须要其中某几个相似的功能时,代码还不能复用。。。

4.3 该怎么使用高阶组件?

高阶组件其实共分为两种模式:属性代理 和 反向继承。分别对应上文中的第一个、第二个例子。那该怎么区分使用呢?嘿嘿,本身用用就知道了。看的再多,不如本身动手写一个来的理解更深。本文不是高阶组件的使用教程,只是两个用高阶组件解决实际问题的例子而已。要真想进一步深刻了解高阶组件,能够看介绍高阶组件的文章,而后动手实践慢慢体会~ 等到你回过头来再想一下的时候,一定会有一种豁然开朗的感受。

5. 写在最后

都说高阶组件大法好,之前都嗤之以鼻,直到抱着试一试的心态才发现。。。

真香真香。。。

相关文章
相关标签/搜索