高阶组件的这种写法的诞生来自于社区的实践,目的是解决一些交叉问题(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技术来解决此类问题所带来的困扰远高于其本身的价值。更多资料可以查阅官方的说明。

高阶函数的定义

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

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

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

x ==> -5
y ==> 6
f ==> abs
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11

用代码验证一下:

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

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

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

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

  • 输出一个函数

高阶组件的定义

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

<!-- more -->

一个简单的高阶组件

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

export default WrappedComponent => class HOC extends Component {
render() {
return (
<fieldset>
<legend>默认标题</legend>
<WrappedComponent {...this.props} />
</fieldset>
);
}
};

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

export default class Demo extends Component {
render() {
return (
<div>
我是一个普通组件
</div>
);
}
} const WithHeaderDemo = withHeader(Demo);

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

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

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

const getDisplayName = component => component.displayName || component.name || 'Component';

export default WrappedComponent => class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() {
return (
<fieldset>
<legend>默认标题</legend>
<WrappedComponent {...this.props} />
</fieldset>
);
}
};

再次观察React DOM Tree

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

为高阶组件传参

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

export default (WrappedComponent, title = '默认标题') => class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() {
return (
<fieldset>
<legend>{title}</legend>
<WrappedComponent {...this.props} />
</fieldset>
);
}
};

之后我们进行调用:

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

此时观察React DOM Tree

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

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

export default (title = '默认标题') => WrappedComponent => class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() {
return (
<fieldset>
<legend>{title}</legend>
<WrappedComponent {...this.props} />
</fieldset>
);
}
}; const WithHeaderDemo = withHeader('高阶组件添加标题')(Demo);

常见的HOC 实现方式

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

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

export default function GenerateId(WrappedComponent) {
return class HOC extends Component {
static displayName = `PropsBorkerHOC(${getDisplayName(WrappedComponent)})`; render() {
const newProps = {
id: Math.random().toString(36).substring(2).toUpperCase()
}; return createElement(WrappedComponent, {
...this.props,
...newProps
});
}
};
}

调用GenerateId:

const PropsBorkerDemo = GenerateId(Demo);

之后我们观察React Dom Tree

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

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

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

export default function (WrappedComponent) {
return class Enhancer extends WrappedComponent {
static displayName = `InheritanceHOC(${getDisplayName(WrappedComponent)})`; componentWillMount() {
// 可以方便地得到state,做一些更深入的修改。
this.setState({
innerText: '我被Inheritance修改了值'
});
} render() {
return super.render();
}
};
}

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

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

使用高阶组件遇到的问题

静态方法丢失

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

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

之后调用 HOC

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

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

export default (title = '默认标题') => (WrappedComponent) => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() {
return (
<fieldset>
<legend>{title}</legend>
<WrappedComponent {...this.props} />
</fieldset>
);
}
} HOC.getDisplayName = WrappedComponent.getDisplayName; return HOC;
};

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

import hoistNonReactStatic from 'hoist-non-react-statics';

export default (title = '默认标题') => (WrappedComponent) => {
class HOC extends Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`; render() {
return (
<fieldset>
<legend>{title}</legend>
<WrappedComponent {...this.props} />
</fieldset>
);
}
} // 拷贝静态方法
hoistNonReactStatic(HOC, WrappedComponent); return HOC;
};

Refs属性不能传递

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

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

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

class Demo extends Component {
static propTypes = {
getRef: PropTypes.func
} static getDisplayName() {
return 'Demo';
} constructor(props) {
super(props);
this.state = {
innerText: '我是一个普通组件'
};
} render() {
const { getRef, ...props } = this.props;
return (
<div ref={getRef} {...props}>
{this.state.innerText}
</div>
);
}
}

之后我们进行调用:

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

虽然这并不是最完美的解决方案,但是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. Android笔记(二十三) Android中的ProgressBar(进度条)

    圆形进度条和水平进度条 进度条也是UI界面一种非常实用的组件,通常用于向用户显示某个耗时操作完成的百分比,进度条可以动态的显示进度,避免长时间的执行某个耗时操作时,让用户感觉程序失去了相应,从而更好的 ...

  2. WebClient 与HttpClient 的区别

    需要搜索下资料. -------------------------------------------------- 微软文档介绍,新的开发中推荐使用:HttpClient WebClient 文档 ...

  3. 9.1: 请每个小组在 cnblogs.com 上面创建团队博客,第一个博客把小组成员都介绍一下 #团队博客作业1------------------答题者:徐潇瑞

    大家好,我们这个团队的名字叫14组之家,之所以这么叫,因为我们是在章亦葵老师的现代软件工程课上成立为14组,而且我们每个组员在这个小家庭中都感受到温暖,我们互相协作,互相配合,来完成我们的任务. 首先 ...

  4. 2019-ACM-ICPC-南昌区网络赛-H. The Nth Item-特征根法求通项公式+二次剩余+欧拉降幂

    2019-ACM-ICPC-南昌区网络赛-H. The Nth Item-特征根法求通项公式+二次剩余+欧拉降幂 [Problem Description] ​ 已知\(f(n)=3\cdot f(n ...

  5. JDK源码那些事儿之LinkedBlockingQueue

    今天继续讲解阻塞队列,涉及到了常用线程池的其中一个队列LinkedBlockingQueue,从类命名部分我们就可以看出其用意,队列中很多方法名是通用的,只是每个队列内部实现不同,毕竟实现的都是同一个 ...

  6. 前端知识总结--html

    1.  doctype的作用是什么? <!DOCTYPE> 声明必须是 HTML 文档的第一行,位于 <html> 标签之前. <!DOCTYPE> 声明不是 HT ...

  7. Python Scrapy 爬虫框架实例

    之前有介绍 scrapy 的相关知识,但是没有介绍相关实例,在这里做个小例,供大家参考学习. 注:后续不强调python 版本,默认即为python3.x. 爬取目标 这里简单找一个图片网站,获取图片 ...

  8. centos 安装mysql5.7.18.tar.gz

    1.解压mysql.tar.gz tar -zxvf mysql--linux-glibc2.-x86_64.tar.gz  2.添加mysql用户组和mysql用户,命令如下: groupadd m ...

  9. Turn Off Windows Firewall Using PowerShell and CMD

    If you want to turn off the Windows Firewall, there are three methods. One is using the GUI which is ...

  10. 【原创】go语言学习(十九)测试用例

    目录 单元测试 压力测试 Go test命令介绍 单元测试 1.自动化测试框架 testingA. testing包提供了自动化测试相关的框架B. 支持单元测试和压力测试 import ( “test ...