React躬行记(10)——高阶组件

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

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


3.png


图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)高阶组件创建的新组件不会包含原组件的静态方法,如果需要,那么就得手动复制。



相关文章
|
5天前
|
前端开发 JavaScript 测试技术
React 分页组件 Pagination
本文介绍了如何在 React 中从零构建分页组件,涵盖基础概念、常见问题及解决方案。通过示例代码详细讲解了分页按钮的创建、分页按钮过多、初始加载慢、状态管理混乱等常见问题的解决方法,以及如何避免边界条件、性能优化和用户反馈等方面的易错点。旨在帮助开发者更好地理解和掌握 React 分页组件的开发技巧,提升应用的性能和用户体验。
24 0
|
1月前
|
前端开发 开发者
React 函数组件与类组件对比
【10月更文挑战第4天】本文详细比较了React中的函数组件与类组件。函数组件是一种简单的组件形式,以纯函数的形式返回JSX,易于理解与维护,适用于简单的UI逻辑。类组件则是基于ES6类实现的,需要重写`render`方法并能利用更多生命周期方法进行状态管理。文章通过示例代码展示了两者在状态管理与生命周期管理上的差异,并讨论了常见的问题如状态更新异步性与生命周期管理的复杂性,最后给出了相应的解决方法。通过学习,开发者可以根据具体需求选择合适的组件类型。
55 8
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
162 2
|
2月前
|
前端开发 JavaScript 网络架构
react对antd中Select组件二次封装
本文介绍了如何在React中对Ant Design(antd)的Select组件进行二次封装,包括创建MSelect组件、定义默认属性、渲染Select组件,并展示了如何使用Less进行样式定义和如何在项目中使用封装后的Select组件。
89 2
react对antd中Select组件二次封装
|
9天前
|
移动开发 前端开发 API
React 拖拽组件 Drag & Drop
本文介绍了在 React 中实现拖拽功能的方法,包括使用原生 HTML5 Drag and Drop API 和第三方库 `react-dnd`。通过代码示例详细讲解了基本的拖拽实现、常见问题及易错点,帮助开发者更好地理解和应用拖拽功能。
32 9
|
4天前
|
前端开发 UED 开发者
React 分页组件 Pagination
本文介绍了如何在 React 中实现分页组件,从基础概念到常见问题及解决方案。分页组件用于将大量数据分成多个页面,提升用户体验。文章详细讲解了分页组件的基本结构、快速入门步骤、以及如何处理页面跳转不平滑、页码过多导致布局混乱、边界条件处理和数据加载延迟等问题。通过本文,读者可以全面了解并掌握 React 分页组件的开发技巧。
8 2
|
8天前
|
设计模式 前端开发 编译器
与普通组件相比,React 泛型组件有哪些优势?
与普通组件相比,React 泛型组件有哪些优势?
23 6
|
16天前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
33 2
|
23天前
|
前端开发 JavaScript 测试技术
React 高阶组件 (HOC) 应用
【10月更文挑战第16天】高阶组件(HOC)是 React 中一种复用组件逻辑的方式,通过接受一个组件并返回新组件来实现。本文介绍了 HOC 的基础概念、核心功能和常见问题,包括静态方法丢失、ref 丢失、多个 HOC 组合和 props 冲突的解决方案,并提供了具体的 React 代码示例。通过本文,读者可以更好地理解和应用 HOC,提高代码的复用性和可维护性。
52 8
|
22天前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。