热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

浅谈React高阶组件

浅谈React高阶组件-前文5、6月一直忙着自己的琐事,7月(7月31也还是7月嘛)抽空整理一下旧的内容,有之前的读者提到想了解下高阶组件这块的知识点,就刚好整理了一下。高阶

前文

5、6月一直忙着自己的琐事,7月(7月31也还是7月嘛)抽空整理一下旧的内容,有之前的读者提到想了解下高阶组件这块的知识点,就刚好整理了一下。

高阶组件 HOC(HigherOrderComponent) ,听起来像是个一个 React 的高级功能,但是实际上它不属于 React API ,而应该归为一个是使用技巧或者说设计模式。首先直击本质:

高阶组件是一个函数, 并且是一个“参数为组件,返回值为新组件的”的函数。 更直白一点如下:

Fn(组件) => 有更强功能地新组件

这里的Fn 就是高阶组件。

组件是 React 中的一个基本单元,通常它接受一些 props 属性,并最终展示为具体的UI,但是某些场景下传统组件还不足以解决问题。

在此分割线下 ,我们可以暂时抛开枯燥的代码,聊一聊生活中一个常见的场景 - 点奶茶。 在当下生活里,奶茶成为很多人生活中的快乐调剂(毕竟生活已经这么苦了-_-),而品种口味也是多种多样的,比如说基础的分类就有纯茶、奶茶、咖啡、鲜榨果汁等,加料也是五花八门,有芝士,牛乳、干果碎,芋泥等.... (嗯 我准备先点一杯奶茶,喝完回来继续写)

好的,我回来了~

那么现在就可以抽象出几个基础组件:

  • 纯茶
  • 奶茶
  • 果茶

它们分别都可搭配以下加料:

  • 加芝士
  • 加碧根果碎

对于不同基础茶加料的逻辑行为,是相似的,所以这两种加料方式就可以设计为高阶组件,这样可以很方便地根据需要生成不同类型地最终奶茶。套用前面地函数表达式也就是:

Fn(基础奶茶) => 不同风味的奶茶

这里的Fn也就是加料函数。它的作用是让一款基本的奶茶通过加料变成一款增强的奶茶。

正文

到此,相信大家对高阶函数的作用有个大概的概念了, 接下来进入正题(醒醒 枯燥的来了)。

从一个常见场景说起

相信前端同学都写过不少后台系统,自然免不了某些常见功能,比如操作日志打印,权限控制等,以操作日志打印为例,要实现以下需求:在进入某些页面组件时,需要打印出日志并传送至服务器。

class Page1 extends React.Component {
  componentDidMount() {
    // 用console.log来模拟日志打印 实际上这里一般会传到服务器保存
    console.log('进入page1');
  }
  
  render(){
    return <div>page1div>
  }
}

class Page2 extends React.Component {
  componentDidMount() {
    console.log('进入page2');
  }
  
  render(){
    return <div>page2div>
  }
}

观察这 Page1 Page2 两种组件都存在一部分相似的逻辑:在 componentDidMount 阶段,需要console.log 当前的页面名称。

现在把这部分逻辑移动到一个函数里面:

function withLog (WrappedComponent, pageName) {
   // 这个函数接收一个组件作为参数
   return class extends React.Component {
     componentDidMount() {
       // 用console.log来模拟日志打印 实际上这里一般会传到服务器保存
       console.log(pageName);
     }
     render (){
       // 此处注意要把this.props继续透传下去
       return <WrappedComponent {...this.props} />;
     }
   }
}

此时可以解耦掉打印日志的逻辑和具体组件的关联:

class Page1 extends React.Component {
  // 不必保留打印日志的逻辑了
  render(){
    return <div>page1div>
  }
}

class Page2 extends React.Component {
  // 不必保留打印日志的逻辑了

  render(){
    return <div>page2div>
  }
}

// 使用时
const Page1WithLog = withLog(Page1);
const Page2WithLog = withLog(Page2);

这样,就实现了一个简单的高阶组件!

高阶组件做了什么

从上面的例子可以看到,高阶组件是将传入的组件,包装在容器组件(容器组件就是 withLog函数中 return 的匿名组件)中,最后返回了一个有增强功能的新组件。 这里有个很关键的地方是:

  • 不要修改原先的原先的组件!
  • 不要修改原先的原先的组件!
  • 不要修改原先的原先的组件!

熟悉React的同学会发现,它处处都贯彻着函数式编程的思想,同样的,高阶组件必须是个纯函数(相同输入必须返回相同的结果) 这样才能保证组件的可复用性,

高阶组件的组合使用

前面介绍了单独使用一个高阶组件的情况,那么如果要同时使用多个高阶组件呢? 延续前面的例子,我们再设计一个提供权限管理功能的高阶组件:

function withCheckPermission (WrappedComponent){
    return class extends React.Component {
       async componentWillMount() {
         const hasPermission = await getCurrentUserPermission();
         this.setState({hasPermission});
       }
       render (){
         // 此处注意要把this.props继续透传下去
         return (
           {this.state.hasPermission ? 
            <WrappedComponent {...this.props} /> :
           <div>您没有权限查看该页面,请联系管理员!div>}
         )
       }
   }
}

checkPermission 函数会检查用户权限,以判断是否允许用户访问当前页面,接下来我们希望给前面 Page1 组件同时加上权限控制和日志打印的功能:

// 当然可以这样写

// 1. 首先附加Log功能
const Page1WithLog = withLog(Page1, 'pageName1');
// 2. 在1的基础上附加CheckPermission功能
const Page1WithLogAndPermission = withCheckPermission(Page1WithLog);

实际上可以直接用 compose 来实现, 这样在使用多个高阶组件时可以更简洁:
// tips:  compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
import {compose} from 'redux';
const Page1WithLogAndPermission = compose(
  Page1WithLogAndPermission,
  (Component) => withLog(Component, 'pageName1'),
);

在前面提到过,高阶组件不会破坏被包裹组件本身,因此非常适合灵活使用多个组件,实际上产生的效果很类似于在原有的组件外层再包裹了不同的组件。

使用命名来方便调试

由于高阶组件会在 WrapComponent 外层包裹组件,那么在使用的过程,为了方便调试,就很有必要给每个高阶组件设置 displayName 属性,以前面的withLog 为例:

function withLog (WrappedComponent, pageName) {
   return class extends React.Component {
     static displayName = `withLog(${getDisplayName(WrappedComponent)})`;
     componentDidMount() {
       // 用console.log来模拟日志打印 实际上这里一般会传到服务器保存
       console.log(pageName);
     }
     render (){
       // 此处注意要把this.props继续透传下去
       return <WrappedComponent {...this.props} />;
     }
   }
}

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

这样的话调试过程就可以通过调试器的属性,轻易找到最终代码里的每一层组件。

注意事项

注意事项其实大部分和高阶组件的实现本质有关, 文中一直在强调,高阶组件的实质是: 用一个新的组件包裹原有的 WrappedComponent 组件,并在新组件增加一些行为,那么包裹势必也会带来一些注意点。

注意传递props

传递props 的意义自然不必多说,除高阶组件本身需要的一些专属props以外,其他的props要继续返回给WrappedComponent ,如下:

function withSomeFeature (WrappedComponent){
    return class extends React.Component {
       // 省略功能
       render (){
         // 此处注意 extraProp表示仅仅是当前这个高阶函数要用的props 
         const { extraProp, ...passThroughProps } = this.props;
         
         // 要把剩下和自己无关的props继续透传下去 
         return<WrappedComponent {...passThroughProps } />
       }
   }
}

不要在render中使用高阶组件

具体来说不要这样使用:

class Test extends React.Component {
  render() {
    const EnhancedCompOnent= enhance(MyComponent);
    return <EnhancedComponent />;
  }
}

在上面的代码中,每次执行render 时, const EnhancedCompOnent= enhance(MyComponent); 返回的是不同的新组件(因为组件解析最后实际是一个object,也就是一个引用类型的值,所以每次定义相当于是重新生成一个对象),这样导致的结果是,每次该组件和它的所有子组件状态完全丢失。 所以正确的用法是在组件之外,用高阶组件生成所需要的新组件后直接使用新的组件:

const EnhancedCompOnent= enhance(MyComponent);

class Test extends React.Component {
  render() {
    return <EnhancedComponent />;
  }
}

拷贝静态方法

同样,这也是包裹带来的问题, 假设 WrappedComponent 上有个非常好用的方法,但是经过高阶组件的增强后,如果不加处理,方法就丢失了:

// WrappedComponent原有一些方法
WrappedComponent.staticMethod = function() {/*...*/}

// 使用 HOC
const EnhancedCompOnent= enhance(WrappedComponent);
// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true

解决方案就是去复制静态方法,常见复制的方法有两种:
明确知道有哪些静态方法要拷贝, 然后用 Enhance.staticMethod = WrappedComponent.staticMethod; 逐个拷贝;
使用 hoist-non-react-statics 自动拷贝:
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  // 核心代码
  hoistNonReactStatic(Enhance, WrappedComponent);
  return Enhance;
}

处理Refs

对于部分使用了 refsWrapComponent ,这是无法直接像 props 的属性一样去透传的, 这个应该使用 React.forwardRef API(React 16.3 中引入)来进行处理。 ref 的特殊之处后面在其他文章里做详细描述。

聊一下反向继承

在文章末尾,也顺带说一下反向继承, 之所以放在最后,是因为这种方式并不是 React 官方推崇的方式,官方文档有这么一句:

请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

但是看到挺多现有的文章都介绍过这种用法,那就顺便简单介绍下,仅仅作为了解,并不推荐使用(我目前实践里也尚未遇到非用这种不可的场景,如果碰到后面另行补充)。

回顾一下前面提到高阶组件的时候,提到的一直是** 装饰者模式 用新组建包裹旧组件做增强** 等关键词,与此不同的是,反向代理的思路是这样的,直接上示例代码:

function withHeader (WrappedComponent){
    // 请注意这里是extends WrappedComponent而不是extends React.Component
    return class extends WrappedComponent {
       render (){
         <div>
            <h1 className='demo-header'>h1>
            // 注意此处要调用父类的render函数
            { super.render() }
         div>
       }
   }
}

观察这个例子的重点部分: 高阶组件实际上是返回了一个继承WrappedComponent组件的新组件,这也是反向继承命名的由来。在这种模式下,主要可以有两种操作:

  • 渲染劫持,如上图的例子可见,在返回的新组件里其实可以控制WrappedComponentrender结果并且执行各种需要的操作,包括选择性的渲染WrappedComponent的子树
  • 操作 state ,由于在新组件可以通过this访问到WrappedComponent,所以同样可以通过this.state 来修改它。

这种实现高阶组件的方式如果真的要使用,一定要非常谨慎,渲染劫持需要考虑条件渲染(即不完全返回子树)的情况,而操作state也有可能在某些情况下破坏父组件的原有逻辑。

谨慎使用,谨慎使用,谨慎使用

总结

水着水着又到结尾了,简单回顾下本文的主要内容:

  • 高阶组件的本质是一个函数,入参和返回值均为组件,作用是给组件增强某个特定的功能
  • 高阶组件推荐灵活组合的使用方式
  • 使用过程中要记得一些注意事项
  • 大概了解反向继承的原理,但是要谨慎使用

对于ref这块挖了个坑,因为真要写起来还是蛮多内容的,本着每篇文章应该主题清晰,内容简练,让读者10分钟内学到知识的准则,决定还是后面单独再写

最后的最后,首先是感谢每个关注的读者朋友(尤其是这位催更的读者,有机会请你喝奶茶),欢迎大家关注专栏,也希望大家对于喜爱的文章,能够不吝点赞和收藏,对于行文风格和内容有任何意见的,都欢迎私信交流。


推荐阅读
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 本文介绍了在wepy中运用小顺序页面受权的计划,包含了用户点击作废后的从新受权计划。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • PHP中的单例模式与静态变量的区别及使用方法
    本文介绍了PHP中的单例模式与静态变量的区别及使用方法。在PHP中,静态变量的存活周期仅仅是每次PHP的会话周期,与Java、C++不同。静态变量在PHP中的作用域仅限于当前文件内,在函数或类中可以传递变量。本文还通过示例代码解释了静态变量在函数和类中的使用方法,并说明了静态变量的生命周期与结构体的生命周期相关联。同时,本文还介绍了静态变量在类中的使用方法,并通过示例代码展示了如何在类中使用静态变量。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • 本文介绍了使用cacti监控mssql 2005运行资源情况的操作步骤,包括安装必要的工具和驱动,测试mssql的连接,配置监控脚本等。通过php连接mssql来获取SQL 2005性能计算器的值,实现对mssql的监控。详细的操作步骤和代码请参考附件。 ... [详细]
author-avatar
用释怀来成全悲伤_490_905_560
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有