概述
高级组件到底能够解决什么问题?举一个特别简单的例子,话说小明负责开发一个 web 应用,应用的结构如下所示,而且这个功能小明已经开发完了。
但是,有一天老板突然提出了一个权限隔离的需求,就是部分模块组件受到权限控制,后台的数据交互的结果权限控制着模块展示与否,而且没有权限会默认展示无权限提示页面。(如下图,黄色部分是受到权限控制的组件模块)
那么小明面临的问题是,如何给需要权限隔离的模块,绑定权限呢?那第一种思路是把所有的需要权限隔离的模块重新绑定权限,通过权限来判断组件是否展示。
这样无疑会给小明带来很多的工作量,而且后续项目可能还有受权限控制的页面或者组件,都需要手动绑定权限。那么如何解决这个问题呢,思考一下,既然是判断权限,那么可以把逻辑都写在一个容器里,然后将每个需要权限的组件通过容器包装一层,这样不就不需要逐一手动绑定权限了吗?所以 HOC 可以合理的解决这个问题,通过 HOC 模式结构如下图所示:
综上所述,HOC的产生根本作用就是解决大量的代码复用,逻辑复用问题。既然说到了逻辑复用,那么具体复用了哪些逻辑呢?
- 首先第一种就是像上述的拦截问题,本质上是对渲染的控制,对渲染的控制可不仅仅指是否渲染组件,还可以像 dva 中 dynamic 那样懒加载/动态加载组件。
- 还有一种场景,比如项目中想让一个非 Route 组件,也能通过 props 获取路由实现跳转,但是不想通过父级路由组件层层绑定 props ,这个时候就需要一个 HOC 把改变路由的 history 对象混入 props 中,于是 withRoute 诞生了。所以 HOC 还有一个重要的作用就是让 props 中混入一些你需要的东西。
- 还有一种情况,如果不想改变组件,只是监控组件的内部状态,对组件做一些赋能,HOC 也是一个不错的选择,比如对组件内的点击事件做一些监控,或者加一次额外的生命周期
高阶函数:
- 接受一个或多个函数作为输入
- 输出一个函数
JavaScript中比较常见的filter、map、reduce都是高阶函数
高阶组件:
- HOC (higher order component)
- 高阶组件是参数为组件,返回值为新组件的函数
- 在有了hook函数之后就很少用高阶组件了
我们可以进行如下的解析:
- 首先, 高阶组件 本身不是一个组件,而是一个函数;
- 其次,这个函数的参数是一个组件,返回值也是一个组件
使用高级组件的目的:
- 目的:实现状态逻辑复用 增强一个组件的能力
- 采用 包装(装饰)模式 ,比如说:手机壳
- 手机:获取保护功能
- 手机壳 :提供保护功能
- 高阶组件就相当于手机壳,通过包装组件,增强组件功能
思路分析
- 高阶组件(HOC,Higher-Order Component)是一个函数,接收要包装的组件,返回增强后的组件
- 高阶组件的命名,通常:
withMouse
withRouter
withXXX
- 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给
被包装组件
使用步骤
- 创建一个函数,名称约定以 with 开头
- 指定函数参数(作为要增强的组件) 传入的组件只能渲染基本的UI
- 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
- 在内部创建的组件的render中,需要渲染传入的基本组件,增强功能,通过props的方式给基本组件传值
- 调用该高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
两种不同的高阶组件
常用的高阶组件有属性代理和反向继承两种,两者之间有一些共性和区别。接下来分别介绍一下两种模式下的高阶组件。
属性代理
属性代理,就是用组件包裹一层代理组件,在代理组件上,可以做一些,对源组件的强化操作。这里注意属性代理返回的是一个新组件,被包裹的原始组件,将在新的组件里被挂载。
优点:
- ① 属性代理可以和业务组件低耦合,零耦合,对于条件渲染和 props 属性增强,只负责控制子组件渲染和传递额外的 props 就可以了,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的 HOC ,目前开源的 HOC 基本都是通过这个模式实现的。
- ② 同样适用于类组件和函数组件。
- ③ 可以完全隔离业务组件的渲染,因为属性代理说白了是一个新的组件,相比反向继承,可以完全控制业务组件是否渲染。
- ④ 可以嵌套使用,多个 HOC 是可以嵌套使用的,而且一般不会限制包装 HOC 的先后顺序。
缺点:
- ① 一般无法直接获取原始组件的状态,如果想要获取,需要 ref 获取组件实例。
- ② 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
- ③ 因为本质上是产生了一个新组件,所以需要配合 forwardRef 来转发 ref。
反向继承
反向继承和属性代理有一定的区别,在于包装后的组件继承了原始组件本身,所以此时无须再去挂载业务组件。
优点:
- ① 方便获取组件内部状态,比如 state ,props ,生命周期,绑定的事件函数等。
- ② es6继承可以良好继承静态属性。所以无须对静态属性和方法进行额外的处理。
缺点:
- ① 函数组件无法使用。
- ② 和被包装的组件耦合度高,需要知道被包装的原始组件的内部状态,具体做了些什么?
- ③ 如果多个反向继承 HOC 嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个 componentDidMount ,当前 componentDidMount 会覆盖上一个 componentDidMount 。这样副作用串联起来,影响很大。
基本使用
接收一个组件作为参数,返回一个新组件
import React, { PureComponent } from 'react'
import Home from './Home'
// 定义高阶组件
function hoc(WrapperComponent) {
// 1. 定义一个类组件
class NewComponent extends PureComponent {
render() {
return <WrapperComponent></WrapperComponent>
}
}
return NewComponent
// 2. 定义一个函数组件
// function NewComponent2(props) {}
// return NewComponent2
}
const HomeHoc = hoc(Home)
export class classHello extends PureComponent {
render() {
return (
<div>
<HomeHoc />
</div>
)
}
}
export default classHello
Home.jsx
import React, { PureComponent } from 'react';
class Home extends PureComponent {
render() {
return (
<div>
122
</div>
);
}
}
export default Home;
高阶组件应用
应用场景
基础使用
为什么React引入高阶组件的概念?它到底有何威力?让我们先通过一个简单的例子说明一下。
假设有一个组件MyComponent
,需要从LocalStorage
中获取数据,然后渲染数据到界面。我们可以这样写组件代码:
代码很简单,但当有其他组件也需要从LocalStorage
中获取同样的数据展示出来时,需要在每个组件都重复componentWillMount
中的代码,这显然是很冗余的。下面让我们