资讯专栏INFORMATION COLUMN

高阶组件HOC - 小试牛刀

lookSomeone / 3365人阅读

摘要:因为这个高阶函数,赋予了普通组件一种呼吸闪烁的能力记住这句话,圈起来重点考。其实,高阶组件就是把一些通用的处理逻辑封装在一个高阶函数中,然后返回一个拥有这些逻辑的组件给你。

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

1. 前言

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

2. 一个中心

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

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

3. 两个例子 3.1 例子1:呼吸动画

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

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

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

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

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 = () => ;

}

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

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

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之后,才发现真香真香。。。

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

接收什么?当然是接收刚才renderContent返回的那个组件啦。

返回什么?当然是返回我们的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竟然以props的形式传给了它。只要WrappedComponent拿到了关键的opacity,那岂不是想干什么就干什么来着,而且还没有前面说的什么匿名函数和domDiff消耗问题。再配上decorator装饰器,岂不是美滋滋?代码如下:

@WithLoading()
class Test extends React.PureComponent {
  render() {
    const {opacity} = this.props;
    return (
      
        
          
          
        
        
          
          
        
      
    )
  }
}

相比之下,显然高阶组件的用法更胜一筹。以后不管要做成什么样的呼吸动画,只要加一个@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 = () => 
; _renderBody = () => ; _renderFooter = () =>
; render = () => ( {this._renderHeader()} {this._renderBody()} {this._renderFooter()} ); }

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

class X extends Page {

  // ...省略

  _renderHeaderA = () => ;

  _renderBodyA = () => ;

  _renderFooterA = () => ;

  _renderHeaderB = () => ;

  _renderBodyB = () => ;

  _renderFooterB = () => ;

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

可是这种处理方式有一个很大的弊端!作为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. 写在最后

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

真香真香。。。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98175.html

相关文章

  • 高阶组件 + New Context API = ?

    摘要:但是老版的存在一个严重的问题子孙组件可能不更新。其中,某一时刻属性发生变化导致组件触发了一次渲染,但是由于组件是且并未用到属性,所以的变化不会触发及其子孙组件的更新,导致组件未能得到及时的更新。 1. 前言 继上次小试牛刀尝到高价组件的甜头之后,现已深陷其中无法自拔。。。那么这次又会带来什么呢?今天,我们就来看看【高阶组件】和【New Context API】能擦出什么火花! 2. N...

    Joyven 评论0 收藏0
  • 动手写个React高阶组件

    摘要:作用是给组件增减属性。如果你的高阶组件不需要带参数,这样写也是很的。那么需要建立一个引用可以对被装饰的组件做羞羞的事情了,注意在多个高阶组件装饰同一个组件的情况下,此法并不奏效。你拿到的是上一个高阶组件的函数中临时生成的组件。 是什么 简称HOC,全称 High Order Component。作用是给react组件增减props属性。 怎么用 为什么不先说怎么写?恩,因为你其实已经用...

    xiaokai 评论0 收藏0
  • React中的函数子组件(FaCC)和高阶组件(HOC)

    摘要:高阶函数我们都用过,就是接受一个函数然后返回一个经过封装的函数而高阶组件就是高阶函数的概念应用到高阶组件上使用接受一个组件返回一个经过包装的新组件。灵活性在组合阶段相对更为灵活,他并不规定被增强组件如何使用它传递下去的属性。 在接触过React项目后,大多数人都应该已经了解过或则用过了HOC(High-Order-Components)和FaCC(Functions as Child ...

    elliott_hu 评论0 收藏0
  • 写给自己的React HOC(高阶组件)手册

    前言 HOC(高阶组件)是React中的一种组织代码的手段,而不是一个API. 这种设计模式可以复用在React组件中的代码与逻辑,因为一般来讲React组件比较容易复用渲染函数, 也就是主要负责HTML的输出. 高阶组件实际上是经过一个包装函数返回的组件,这类函数接收React组件处理传入的组件,然后返回一个新的组件. 注意:前提是建立在不修改原有组件的基础上. 文字描述太模糊,借助于官方...

    W4n9Hu1 评论0 收藏0
  • Render props、render callback 和高阶组件皆可互换

    摘要:现在来看看怎么使用高阶组件来达到同样的目的。在这个新的组件里包含了加强的和等内容。有时会遇到一个提供了的库,但是你喜欢的是高阶组件。我们来根据上面的例子来加一些方法可以让高阶组件和模式可以互相转换。总结,回调绘制和高阶组件都是可以互换的。 让 render-xxx 模式都可以互换。 基础 所有上面提到的三种模式都是为了处理 mixin 要处理的问题的。在我们继续之前,我们来看一些例子。...

    姘搁『 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<