高阶组件的这种写法的诞生来自于社区的实践,目的是解决一些交叉问题(Cross-Cutting Concerns)。而最早时候 React 官方给出的解决方案是使用 mixin 。而 React 也在官网中写道:

We previously recommended mixins as a way to handle cross-cutting concerns. We've since realized that mixins create more trouble than they are worth.

官方明显也意识到了使用mixins技术来解决此类问题所带来的困扰远高于其本身的价值。更多资料可以查阅官方的说明。

高阶函数的定义

说到高阶组件,就不得不先简单的介绍一下高阶函数。下面展示一个最简单的高阶函数

  1. const add = (x,y,f) => f(x)+f(y)

当我们调用add(-5, 6, Math.abs)时,参数 x,y 和f 分别接收 -5,6 和 Math.abs,根据函数定义,我们可以推导计算过程为:

  1. x ==> -5
  2. y ==> 6
  3. f ==> abs
  4. f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11

用代码验证一下:

  1. add(-5, 6, Math.abs); //11

高阶在维基百科的定义如下

高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入

  • 输出一个函数

高阶组件的定义

那么,什么是高阶组件呢?类比高阶函数的定义,高阶组件就是接受一个组件作为参数并返回一个新组件的函数。这里需要注意高阶组件是一个函数,并不是组件,这一点一定要注意。
同时这里强调一点高阶组件本身并不是 React API。它只是一种模式,这种模式是由 React 自身的组合性质必然产生的。
更加通俗的讲,高阶组件通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的 React 组件,供其他组件调用。

<!-- more -->

一个简单的高阶组件

下面我们来实现一个简单的高阶组件

  1. export default WrappedComponent => class HOC extends Component {
  2. render() {
  3. return (
  4. <fieldset>
  5. <legend>默认标题</legend>
  6. <WrappedComponent {...this.props} />
  7. </fieldset>
  8. );
  9. }
  10. };

在其他组件中,我们引用这个高阶组件来强化它

  1. export default class Demo extends Component {
  2. render() {
  3. return (
  4. <div>
  5. 我是一个普通组件
  6. </div>
  7. );
  8. }
  9. }
  10. const WithHeaderDemo = withHeader(Demo);

下面我们来看一下React DOM Tree,调用了高阶组件之后,发生了什么:

可以看到,Demo 被 HOC 包裹(wrapped)了之后添加了一个标题默认标题。但是同样会发现,如果调用了多个 HOC 之后,我们会看到很多的HOC,所以应
该做一些优化,也就是在高阶组件包裹(wrapped)以后,应该保留原有的名称。

我们改写一下上述的高阶组件代码,增加一个 getDisplayName 函数,之后为Demo 添加一个静态属性 displayName

  1. const getDisplayName = component => component.displayName || component.name || 'Component';
  2. export default WrappedComponent => class HOC extends Component {
  3. static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
  4. render() {
  5. return (
  6. <fieldset>
  7. <legend>默认标题</legend>
  8. <WrappedComponent {...this.props} />
  9. </fieldset>
  10. );
  11. }
  12. };

再次观察React DOM Tree

可以看到,该组件原本的名称已经显示在React DOM Tree上了。
这个HOC 的功能是为原有的组件添加一个标题,也就是说所有需要添加标题的组件都可以通过调用此 HOC 进行包裹(wrapped) 后实现此功能。

为高阶组件传参

现在,我们的 HOC 已经可以为其他任意组件提供标题了,但是我们还希望可以修改标题中的字段。由于我们的高阶组件是一个函数,所以可以为其添加一个参数title。下面我们对HOC进行改写:

  1. export default (WrappedComponent, title = '默认标题') => class HOC extends Component {
  2. static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
  3. render() {
  4. return (
  5. <fieldset>
  6. <legend>{title}</legend>
  7. <WrappedComponent {...this.props} />
  8. </fieldset>
  9. );
  10. }
  11. };

之后我们进行调用:

  1. const WithHeaderDemo = withHeader(Demo,'高阶组件添加标题');

此时观察React DOM Tree

可以看到,标题已经正确的进行了设置。

当然我们也可以对其进行柯里化:

  1. export default (title = '默认标题') => WrappedComponent => class HOC extends Component {
  2. static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
  3. render() {
  4. return (
  5. <fieldset>
  6. <legend>{title}</legend>
  7. <WrappedComponent {...this.props} />
  8. </fieldset>
  9. );
  10. }
  11. };
  12. const WithHeaderDemo = withHeader('高阶组件添加标题')(Demo);

常见的HOC 实现方式

基于属性代理(Props Proxy)的方式

属性代理是最常见的高阶组件的使用方式,上面所说的高阶组件就是这种方式。
它通过做一些操作,将被包裹组件的props和新生成的props一起传递给此组件,这称之为属性代理。

  1. export default function GenerateId(WrappedComponent) {
  2. return class HOC extends Component {
  3. static displayName = `PropsBorkerHOC(${getDisplayName(WrappedComponent)})`;
  4. render() {
  5. const newProps = {
  6. id: Math.random().toString(36).substring(2).toUpperCase()
  7. };
  8. return createElement(WrappedComponent, {
  9. ...this.props,
  10. ...newProps
  11. });
  12. }
  13. };
  14. }

调用GenerateId:

  1. const PropsBorkerDemo = GenerateId(Demo);

之后我们观察React Dom Tree

可以看到我们通过 GenerateId 顺利的为 Demo 添加了 id

基于反向继承(Inheritance Inversion)的方式

首先来看一个简单的反向继承的例子:

  1. export default function (WrappedComponent) {
  2. return class Enhancer extends WrappedComponent {
  3. static displayName = `InheritanceHOC(${getDisplayName(WrappedComponent)})`;
  4. componentWillMount() {
  5. // 可以方便地得到state,做一些更深入的修改。
  6. this.setState({
  7. innerText: '我被Inheritance修改了值'
  8. });
  9. }
  10. render() {
  11. return super.render();
  12. }
  13. };
  14. }

如你所见返回的高阶组件类(Enhancer)继承了 WrappedComponent。而之所以被称为反向继承是因为 WrappedComponent 被动地被 Enhancer
继承,而不是 WrappedComponent 去继承 Enhancer。通过这种方式他们之间的关系倒转了。

反向继承允许高阶组件通过 this 关键词获取 WrappedComponent,意味着它可以获取到 stateprops,组件生命周期(Component Lifecycle)钩子,以及渲染方法(render)。深入了解可以阅读__@Wenliang__文章中Inheritance Inversion(II)这一节的内容。

使用高阶组件遇到的问题

静态方法丢失

当使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的所有静态方法。
下面为 Demo 添加一个静态方法:

  1. Demo.getDisplayName = () => 'Demo';

之后调用 HOC

  1. // 使用高阶组件
  2. const WithHeaderDemo = HOC(Demo);
  3. // 调用后的组件是没有 `getDisplayName` 方法的
  4. typeof WithHeaderDemo.getDisplayName === 'undefined' // true

解决这个问题最简单(Yǘ Chǚn)的方法就是,将原始组件的所有静态方法全部拷贝给新组件:

  1. export default (title = '默认标题') => (WrappedComponent) => {
  2. class HOC extends Component {
  3. static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
  4. render() {
  5. return (
  6. <fieldset>
  7. <legend>{title}</legend>
  8. <WrappedComponent {...this.props} />
  9. </fieldset>
  10. );
  11. }
  12. }
  13. HOC.getDisplayName = WrappedComponent.getDisplayName;
  14. return HOC;
  15. };

这样做,就需要你清楚的知道都有哪些静态方法需要拷贝的。或者你也可是使用hoist-non-react-statics来帮你自动处理,它会自动拷贝所有非React的静态方法:

  1. import hoistNonReactStatic from 'hoist-non-react-statics';
  2. export default (title = '默认标题') => (WrappedComponent) => {
  3. class HOC extends Component {
  4. static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
  5. render() {
  6. return (
  7. <fieldset>
  8. <legend>{title}</legend>
  9. <WrappedComponent {...this.props} />
  10. </fieldset>
  11. );
  12. }
  13. }
  14. // 拷贝静态方法
  15. hoistNonReactStatic(HOC, WrappedComponent);
  16. return HOC;
  17. };

Refs属性不能传递

一般来说,高阶组件可以传递所有的props属性给包裹的组件,但是不能传递 refs 引用。因为并不是像 key 一样,refs 是一个伪属性,React 对它进行了特殊处理。
如果你向一个由高级组件创建的组件的元素添加 ref 应用,那么 ref 指向的是最外层容器组件实例的,而不是包裹组件。
但有的时候,我们不可避免要使用 refs,官方给出的解决方案是:

传递一个ref回调函数属性,也就是给ref应用一个不同的名字

同时还强调道:React在任何时候都不建议使用 ref应用
改写 Demo

  1. class Demo extends Component {
  2. static propTypes = {
  3. getRef: PropTypes.func
  4. }
  5. static getDisplayName() {
  6. return 'Demo';
  7. }
  8. constructor(props) {
  9. super(props);
  10. this.state = {
  11. innerText: '我是一个普通组件'
  12. };
  13. }
  14. render() {
  15. const { getRef, ...props } = this.props;
  16. return (
  17. <div ref={getRef} {...props}>
  18. {this.state.innerText}
  19. </div>
  20. );
  21. }
  22. }

之后我们进行调用:

  1. <WithHeaderDemo
  2. getRef={(ref) => {
  3. // 该回调函数被作为常规的props属性传递
  4. this.headerDemo = ref;
  5. }}
  6. />

虽然这并不是最完美的解决方案,但是React官方说他们正在探索解决这个问题的方法,能够让我们安心的使用高阶组件而不必关注这个问题。

React 高阶组件浅析的更多相关文章

  1. 聊聊React高阶组件(Higher-Order Components)

    使用 react已经有不短的时间了,最近看到关于 react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡.一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低 ...

  2. 当初要是看了这篇,React高阶组件早会了

    当初要是看了这篇,React高阶组件早会了. 概况: 什么是高阶组件? 高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式. 具体地说 ...

  3. react高阶组件的理解

    [高阶组件和函数式编程] function hello() { console.log('hello jason'); } function WrapperHello(fn) { return fun ...

  4. 函数式编程与React高阶组件

    相信不少看过一些框架或者是类库的人都有印象,一个函数叫什么creator或者是什么什么createToFuntion,总是接收一个函数,来返回另一个函数.这是一个高阶函数,它可以接收函数可以当参数,也 ...

  5. React高阶组件学习笔记

    高阶函数的基本概念: 函数可以作为参数被传递,函数可以作为函数值输出. 高阶组件基本概念: 高阶组件就说接受一个组件作为参数,并返回一个新组件的函数. 为什么需要高阶组件 多个组件都需要某个相同的功能 ...

  6. 利用 React 高阶组件实现一个面包屑导航

    什么是 React 高阶组件 React 高阶组件就是以高阶函数的方式包裹需要修饰的 React 组件,并返回处理完成后的 React 组件.React 高阶组件在 React 生态中使用的非常频繁, ...

  7. react高阶组件的一些运用

    今天学习了react高阶组件,刚接触react学习起来还是比较困难,和大家分享一下今天学习的知识吧,另外缺少的地方欢迎补充哈哈 高阶组件(Higher Order Components,简称:HOC) ...

  8. React——高阶组件

    1.在React中higher-order component (HOC)是一种重用组件逻辑的高级技术.HOC不是React API中的一部分.HOC是一个函数,该函数接收一个组件并且返回一个新组件. ...

  9. react 高阶组件的 理解和应用

    高阶组件是什么东西 简单的理解是:一个包装了另一个基础组件的组件.(相对高阶组件来说,我习惯把被包装的组件称为基础组件) 注意:这里说的是包装,可以理解成包裹和组装: 具体的是高阶组件的两种形式吧: ...

随机推荐

  1. 【ogg三】日常运维篇:清理归档日志,ogg进程注册服务,定期备份数据库

    清理归档日志 ogg使用需要开启归档日志,归档日志会随着时间的推移逐渐增多,占满空间,导致应用无法正常运行. 如果归档日志满了会报错 ORA-00257:archiver error解决办法 检查fl ...

  2. SignalR 初体验

    目录 一.前言 二.服务端 2.1.站点服务端 2.2.宿主服务或客户端 2.3.持久连接和集线器 三.客户端 3.1.使用代理客户端 3.2.不使用代理客户端 一.前言 微软官方给的说明:ASP.N ...

  3. DTcmsV4.0分析学习——(1)数据库结构分析

    数据库名:DTcmsdb4 DTcmsV4.0共35张表(33张表+2张插件表) dt_article 内容管理 dt_article_albums 图片相册 dt_article_attach 附件 ...

  4. EditPlus编译运行java文件

    ok ---------------两张图完成

  5. JavaScript 隐式原型(_proto_)与显示原型(prototype)

    作者:苏墨橘链接:https://www.zhihu.com/question/34183746/answer/59043879来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...

  6. [Dart] Understand Classes and Inheritance in Dart

    We will look at how we can create classes and explore some various features. Dart adopts a single-in ...

  7. HTML 008 head

    HTML <head> 查看在线实例 <title> - 定义了HTML文档的标题使用 <title> 标签定义HTML文档的标题 <base> - 定 ...

  8. MongoDB 建立与删除索引

    1.1 在独立服务器上面建立索引 在独立服务器上面创建索引,可以在空闲时间于后台建立索引. 在后台建立索引,可利用background:true参数运行 >db.foo.ensureIndex( ...

  9. mongodb 导入json文件遇到的坑

    使用mongoimport命令导入外部json文件时,发现一直报错 报错结果如下: json数据格式完全正确如下: 经过再三确认格式最终找到解决方案,原来用cmd导入数据时json  { }包含的数据 ...

  10. Oracle 通过sqlnet.ora文件控制对Oracle数据库的访问

    一.通过sqlnet.ora文件控制对Oracle数据库的访问 出于数据安全考虑,对Oracle数据库的IP做一些限制,只有固定的IP才能访问.修改$JAVA_HOME/NETWORK/ADMIN/s ...