高阶组件(High Order Component,简称HOC)不是一个真的组件,而是一个没有副作用的纯函数,以组件作为参数,返回一个功能增强的新组件,在很多第三方库(例如Redux、Relay等)中都有高阶组件的身影。由于遵循了装饰者模式的设计思想,因此不会入侵传递进来的原组件,而是对其进行抽象、包装和拓展,改变原组件的行为(图9形象的表达出了高阶组件的作用)。这样不仅增强了组件的复用性和灵活性,还保持了组件的易用性。灵活使用高阶组件,可大大提高代码质量。

图9  高阶组件的作用

  高阶组件有两种常见的实现方式:代理和继承,下面会分别做讲解。

一、代理方式

  高阶组件作为原组件的代理,不但会将其包裹住,还会给它添加新特性,并且提供了众多控制原组件的功能,例如操纵props、抽取state、访问实例和再包装等。

1)操纵props

  在原组件(即被包裹组件)接收到props之前,高阶组件可以将其拦截,执行增删改操作,再将处理过的props传给原组件。下面是一个简单的示例,会在高阶组件中新增了一个name属性。

//原组件
class Btn extends React.Component {
render() {
return <button>{this.props.name}</button>;
}
}
//高阶组件
function HOC(Wrapped) {
class Enhanced extends React.Component {
constructor(props) {
super(props);
this.state = { name: "strick" };
}
render() {
return <Wrapped {...this.state} />;
}
}
return Enhanced;
}
const EnhancedBtn = HOC(Btn);

  HOC()函数就是高阶组件,在函数体中声明了用于修饰原组件Wrapped的新组件Enhanced,它的name状态作为props传给了Wrapped,并在render()方法中将Wrapped渲染出来。当执行HOC(Btn)后,就能得到增强了的EnhancedBtn组件。

2)抽取state

  将原组件的state和与之相关的处理函数抽取到高阶组件中,从而使得原组件无状态,变成容易复用的展示型组件。以一个能维护自己状态的Input组件为例,如下所示。

class Input extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.handle = this.handle.bind(this);
}
handle(e) {
this.setState({ value: e.target.value });
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handle} />
);
}
}

  现在将Input组件处理value状态和onChange事件的代码提升到高阶组件中,如下代码所示,在render()方法中初始化了一个newProps对象,用于把处理好的value状态和事件处理程序handle()回传给Input组件。

function stateHOC(Wrapped) {
class Enhanced extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
this.handle = this.handle.bind(this);
}
handle(e) {
this.setState({ value: e.target.value });
}
render() {
let newProps = {
value: this.state.value,
onChange: this.handle
};
return <Wrapped {...newProps} />;
}
}
return Enhanced;
}

  经过高阶组件的抽象后,Input组件就变得很简单,如下代码所示,没有额外的逻辑操作,只要接收传过来的props即可。

class Input extends React.Component {
constructor(props) {
super(props);
}
render() {
return <input type="text" {...this.props} />;
}
}

3)Refs

  通过第5篇提到的Refs访问方式,可以得到被包裹组件的实例,从而就能操纵组件中的DOM元素。以下面的Btn组件为例,在高阶组件中给它定义ref属性,这样就能在新组件的componentDidMount()方法中访问到Btn组件的实例。

class Btn extends React.Component {
render() {
return <button>提交</button>;
}
}
function refHOC(Wrapped) {
class Enhanced extends React.Component {
render() {
return <Wrapped ref={btn => { this.myBtn = btn }} />;
}
componentDidMount() {
console.log(this.myBtn); //Btn组件的实例
}
}
return Enhanced;
}

  注意,ref属性不会传递给原组件,如果在上面的Btn组件中读取this.props.ref,那么得到的值将是undefined,如下所示。

class Btn extends React.Component {
render() {
console.log(this.props.ref); //undefined
}
}

4)包装

  在高阶组件中,还能通过引入其它React元素包装原组件,既能改变布局,也能增加样式。例如用一个包含内边距的<div>元素包裹原组件,并在其邻近的位置新增一个文本框,如下所示。

function wrappedHOC(Wrapped) {
class Enhanced extends React.Component {
render() {
return (
<div style={{ padding: 10 }}>
<input type="text" />
<Wrapped />
</div>
);
}
}
return Enhanced;
}

二、继承方式

  继承是另一种构建高阶组件的方式,即新组件直接继承原组件(如下代码所示),从而实现通用逻辑的复用,并且还能使用原组件的state和props,以及生命周期等方法。

function inheritHOC(Wrapped) {
class Enhanced extends Wrapped { }
return Enhanced;
}

  在代理方式下的新组件和原组件会各自经历一次完整的生命周期,而在继承方式下,两者会共用一次生命周期。

1)渲染劫持

  在高阶组件中,可以通过super.render()渲染原组件,从而就能控制高阶组件的渲染结果,即渲染劫持。例如在新组件的render()方法中克隆原组件并为其传递新的props,如下所示。

function inheritHOC(Wrapped) {
class Enhanced extends Wrapped {
render() {
//获取原组件
const origin = super.render();
//合并原组件的属性,并新增value属性的值
const props = Object.assign({}, origin.props, {value: "strick"});
return React.cloneElement(origin, props, origin.props.children);
}
}
return Enhanced;
}

  代码中的React.cloneElement()方法能接收3个参数,第一个是要克隆的React元素,后两个是要传递的新props和原来的children属性。

  除了render()方法,其余诸如componentWillMount()、componentWillUpdate()等生命周期中的方法也是能劫持的。

2)使用state

  在高阶组件中,不仅可以读取原组件的state,还能对其进行修改或增加,甚至是删除。不过,这三类带有侵略性的操作,会让原组件内部变得混乱不堪,因此要慎用。在下面的示例中,Input组件包含一个value状态,高阶组件内的新组件Enhanced会在其构造函数中增加一个name状态,并修改value状态的值。

class Input extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
}
render() {
return <input type="text" value={this.state.value} />;
}
}
function stateHOC(Wrapped) {
class Enhanced extends Wrapped {
constructor(props) {
super(props);
this.state.name = "strick"; //增加状态
this.state.value = "init"; //修改状态
}
render() {
return super.render();
}
}
return Enhanced;
}
let EnhancedInput = stateHOC(Input);

三、参数传递

  高阶组件除了一个组件参数之外,还能接收其它类型的参数,例如为高阶组件额外传递一个区分类别的type参数,如下所示。

HOC(Wrapped, type)

  不过,在React中,函数式编程的参数传递更为常用,即使用柯里化的形式,如下代码所示,其中HOC(type)会返回一个高阶组件。

HOC(type)(Wrapped)

  而在第三方库中,这种形式的高阶组件被大量应用,例如Redux中用于连接React组件与其Store的connect()函数,它是一个能返回高阶组件的高阶函数,其参数可以是两个函数,如下所示。

const Enhanced = connect(mapStateToProps, mapDispatchToProps)(Wrapped);

  将上面这条语句拆分成两条目的更为清晰的语句,就能让人更容易理解代码的意图,如下所示。

const enhance = connect(mapStateToProps, mapDispatchToProps);
const Enhanced = enhance(Wrapped);

  虽然这种形式的高阶组件会让人困惑,但是更易于组合。因为它会把参数序列处理到只剩一个组件参数,而高阶组件的返回值也是一个组件,也就是说,前一个高阶组件的返回值可以作为后一个高阶组件的参数,从而使得这些高阶组件可以组合在一起。例如有三个高阶组件f、g和h,它们可以像下面这样组合在一起。

f(g(h(Wrapped)))

  如果要嵌套的高阶组件很多,那么这种写法将变得异常丑陋且难以阅读。这个时候,就可以引入compose()函数,它能将函数串联起来,即用平铺的写法实现函数的组合,如下代码所示,省略了compose()函数的具体实现。

compose(f, g, h)

  compose()函数的执行方向是自右向左,并且还有一个限制,那就是第一个高阶组件(即h)可以接收多个参数,但之后的就只能接收一个参数。

四、命名

  在高阶组件中创建的新组件,不会再沿用原组件的名称。为了便于在React Developer Tools中调试,需要为新组件设置一个显示名称,例如新组件的名称是“Enhanced”,原组件的名称是“Input”,那么就以“Enhanced(Input)”为显示名称。

  为了完成这个功能,高阶组件可以修改成下面这样。注意,在定义displayName属性时,用到了ES6新增的模板字面量。

function HOC(Wrapped) {
class Enhanced extends React.Component { }
Enhanced.displayName = `Enhanced(${getDisplayName(Wrapped)})`;
return Enhanced;
}

  getDisplayName()函数用于获取组件的名称(如下代码所示),如果组件的displayName属性不存在,那么就改用name属性,即类或函数的名称。

function getDisplayName(Wrapped) {
return Wrapped.displayName || Wrapped.name || "Component";
}

五、注意事项

(1)不要在组件的render()方法中使用高阶组件。因为高阶组件每次都会创建一个新组件,而根据React的diff算法可知,原组件(即前一次所创建的组件)会先被卸载掉,然后重新挂载新组件。这么做不仅性能低下,而且原组件的状态和其所有子组件都将丢失。

(2)高阶组件创建的新组件不会包含原组件的静态方法,如果需要,那么就得手动复制。

React躬行记(10)——高阶组件的更多相关文章

  1. React躬行记(3)——组件

    组件(Component)由若干个React元素组成,包含属性.状态和生命周期等部分,满足独立.可复用.高内聚和低耦合等设计原则,每个React应用程序都是由一个个的组件搭建而成,即组成React应用 ...

  2. React躬行记(9)——组件通信

    根据组件之间的嵌套关系(即层级关系)可分为4种通信方式:父子.兄弟.跨级和无级. 一.父子通信 在React中,数据是自顶向下单向流动的,而父组件通过props向子组件传递需要的信息是组件之间最常见的 ...

  3. React躬行记(13)——React Router

    在网络工程中,路由能保证信息从源地址传输到正确地目的地址,避免在互联网中迷失方向.而前端应用中的路由,其功能与之类似,也是保证信息的准确性,只不过来源变成URL,目的地变成HTML页面. 在传统的前端 ...

  4. react系列(二)高阶组件-HOC

    高阶组件 简单来说,高阶组件可以看做一个函数,且该函数接受一个组件作为参数,并返回一个新的组件. 我在之前的博客<闭包和类>中提到一个观点,面向对象的好处就在于,易于理解,方便维护和复用. ...

  5. React躬行记(15)——React Hooks

    Hook(钩子)是React v16.8新引入的特性,能以钩子的形式为函数组件附加类组件的状态.生命周期等特性.React的类组件有难以拆分.测试,状态逻辑分散,难以复用等问题,虽然可以通过渲染属性( ...

  6. React躬行记(12)——Redux中间件

    Redux的中间件(Middleware)遵循了即插即用的设计思想,出现在Action到达Reducer之前(如图10所示)的位置.中间件是一个固定模式的独立函数,当把多个中间件像管道那样串联在一起时 ...

  7. React躬行记(8)——样式

    由于React推崇组件模式,因此会要求HTML.CSS和JavaScript混合在一起,虽然这与过去的关注点分离正好相反,但是更有利于组件之间的隔离.React已将HTML用JSX封装,而对CSS只进 ...

  8. React躬行记(5)——React和DOM

    React实现了一套与浏览器无关的DOM系统,包括元素渲染.节点查询.事件处理等机制. 一.ReactDOM 自React v0.14开始,官方将与DOM相关的操作从React中剥离,组成单独的rea ...

  9. React躬行记(14)——测试框架

    测试不仅可以发现和预防问题,还能降低风险.减少企业损失.在React中,涌现了多种测试框架,本节会对其中的Jest和Enzyme做详细的讲解. 一.Jest Jest是由Facebook开源的一个测试 ...

随机推荐

  1. vs2008在win7系统中安装不问题

    据说是office软件冲突问题. 解决方案是卸载了office软件,不管是2007还是其它版本,先安装vs2008,再安装其它的.

  2. hadoop 数据抽取

    #!/bin/bash if [ ! -z $2 ]; then start_time=$1 end_time=$2 else starttime=`date +%Y%m%d%H%M -d '-15 ...

  3. java中静态类与普通类之间区别

    所谓静态,指以static关键字修饰的,包括类,方法,块,字段. 非静态,指没有用static 修饰的. 一.静态类的特点 1.全局唯一,任何一次的修改都是全局性的影响 2.只加载一次,优先于非静态 ...

  4. Java NIO 学习笔记(五)----路径、文件和管道 Path/Files/Pipe

    目录: Java NIO 学习笔记(一)----概述,Channel/Buffer Java NIO 学习笔记(二)----聚集和分散,通道到通道 Java NIO 学习笔记(三)----Select ...

  5. Python开发【第四篇】: 三大器和内置函数

    内容概要 函数名 闭包 迭代器 生成器 推导式与表达式 内置函数 装饰器 初识递归 1.函数名 函数名的运用: 函数名是一个变量,但它是一个特殊的变量,与括号配合可以执行函数的变量. 01. 函数名的 ...

  6. docker命令总结

    用 docker pull 拉取镜像 root@lishichao-virtual-machine:~# docker pull hello-world Using default tag: late ...

  7. CSS3常用选择器

    一.基本选择器 子元素选择器 概念:子元素选择器只能选择某元素的子元素语法格式:父元素 > 子元素 (Father > Children)兼容性:IE8+.FireFox.Chrome.S ...

  8. Effective Java - 构造器私有、枚举和单例

    目录 饿汉式单例 静态常量 静态代码块 懒汉式单例 尝试加锁 同步代码块 双重检查 静态内部类单例 枚举单例 Singleton 是指仅仅被实例化一次的类.Singleton代表了无状态的对象像是方法 ...

  9. 并发编程-concurrent指南-原子操作类-AtomicInteger

    在java并发编程中,会出现++,--等操作,但是这些不是原子性操作,这在线程安全上面就会出现相应的问题.因此java提供了相应类的原子性操作类. 1.AtomicInteger

  10. SpringBoot使用Docker快速部署项目

    1.简介 建议阅读本文最好对Dokcer有一些了解 首先我们先了解一下Docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口.它是目前最流行的 Linux 容器 ...