React中的高阶组件

高阶组件HOCHigher Order ComponentReact中用于复用组件逻辑的一种高级技巧,HOC自身不是React API的一部分,它是一种基于React的组合特性而形成的设计模式。

描述

高阶组件从名字上就透漏出高级的气息,实际上这个概念应该是源自于JavaScript的高阶函数,高阶函数就是接受函数作为输入或者输出的函数,可以想到柯里化就是一种高阶函数,同样在React文档上也给出了高阶组件的定义,高阶组件是接收组件并返回新组件的函数。

A higher-order component is a function that takes a component and returns a new component.

具体而言,高阶组件是参数为组件,返回值为新组件的函数,组件是将props转换为UI,而高阶组件是将组件转换为另一个组件。HOCReact的第三方库中很常见,例如ReduxconnectRelaycreateFragmentContainer

// 高阶组件定义
const higherOrderComponent = (WrappedComponent) => {return class EnhancedComponent extends React.Component {// ...render() {return <WrappedComponent {...this.props} />;}};
}// 普通组件定义
class WrappedComponent extends React.Component{render(){//....}
}// 返回被高阶组件包装过的增强组件
const EnhancedComponent = higherOrderComponent(WrappedComponent);

在这里要注意,不要试图以任何方式在HOC中修改组件原型,而应该使用组合的方式,通过将组件包装在容器组件中实现功能。通常情况下,实现高阶组件的方式有以下两种:

  • 属性代理Props Proxy
  • 反向继承Inheritance Inversion

属性代理

例如我们可以为传入的组件增加一个存储中的id属性值,通过高阶组件我们就可以为这个组件新增一个props,当然我们也可以对在JSX中的WrappedComponent组件中props进行操作,注意不是操作传入的WrappedComponent类,我们不应该直接修改传入的组件,而可以在组合的过程中对其操作。

const HOC = (WrappedComponent, store) => {return class EnhancedComponent extends React.Component {render() {const newProps = {id: store.id}return <WrappedComponent{...this.props}{...newProps}/>;}}
}

我们也可以利用高阶组件将新组件的状态装入到被包装组件中,例如我们可以使用高阶组件将非受控组件转化为受控组件。

class WrappedComponent extends React.Component {render() {return <input name="name" />;}
}const HOC = (WrappedComponent) => {return class EnhancedComponent extends React.Component {constructor(props) {super(props);this.state = { name: "" };}render() {const newProps = {value: this.state.name,onChange: e => this.setState({name: e.target.value}),}return <WrappedComponent {...this.props} {...newProps} />;}}
}

或者我们的目的是将其使用其他组件包裹起来用以达成布局或者是样式的目的。

const HOC = (WrappedComponent) => {return class EnhancedComponent extends React.Component {render() {return (<div class="layout"><WrappedComponent  {...this.props} /></div>);}}
}

反向继承

反向继承是指返回的组件去继承之前的组件,在反向继承中我们可以做非常多的操作,修改stateprops甚至是翻转Element Tree,反向继承有一个重要的点,反向继承不能保证完整的子组件树被解析,也就是说解析的元素树中包含了组件(函数类型或者Class类型),就不能再操作组件的子组件了。
当我们使用反向继承实现高阶组件的时候可以通过渲染劫持来控制渲染,具体是指我们可以有意识地控制WrappedComponent的渲染过程,从而控制渲染控制的结果,例如我们可以根据部分参数去决定是否渲染组件。

const HOC = (WrappedComponent) => {return class EnhancedComponent extends WrappedComponent {render() {return this.props.isRender && super.render();  }}
}

甚至我们可以通过重写的方式劫持原组件的生命周期。

const HOC = (WrappedComponent) => {return class EnhancedComponent extends WrappedComponent {componentDidMount(){// ...}render() {return super.render();  }}
}

由于实际上是继承关系,我们可以去读取组件的propsstate,如果有必要的话,甚至可以修改增加、修改和删除propsstate,当然前提是修改带来的风险需要你自己来控制。在一些情况下,我们可能需要为高阶属性传入一些参数,那我们就可以通过柯里化的形式传入参数,配合高阶组件可以完成对组件的类似于闭包的操作。

const HOCFactoryFactory = (params) => {// 此处操作paramsreturn (WrappedComponent) => {return class EnhancedComponent extends WrappedComponent {render() {return params.isRender && this.props.isRender && super.render();}}}
}

HOC与Mixin

使用MixinHOC都可以用于解决横切关注点相关的问题。
Mixin是一种混入的模式,在实际使用中Mixin的作用还是非常强大的,能够使得我们在多个组件中共用相同的方法,但同样也会给组件不断增加新的方法和属性,组件本身不仅可以感知,甚至需要做相关的处理(例如命名冲突、状态维护等),一旦混入的模块变多时,整个组件就变的难以维护,Mixin可能会引入不可见的属性,例如在渲染组件中使用Mixin方法,给组件带来了不可见的属性props和状态state,并且Mixin可能会相互依赖,相互耦合,不利于代码维护,此外不同的Mixin中的方法可能会相互冲突。之前React官方建议使用Mixin用于解决横切关注点相关的问题,但由于使用Mixin可能会产生更多麻烦,所以官方现在推荐使用HOC
高阶组件HOC属于函数式编程functional programming思想,对于被包裹的组件时不会感知到高阶组件的存在,而高阶组件返回的组件会在原来的组件之上具有功能增强的效果,基于此React官方推荐使用高阶组件。

注意

不要改变原始组件

不要试图在HOC中修改组件原型,或以其他方式改变它。

function logProps(InputComponent) {InputComponent.prototype.componentDidUpdate = function(prevProps) {console.log("Current props: ", this.props);console.log("Previous props: ", prevProps);};// 返回原始的 input 组件,其已经被修改。return InputComponent;
}// 每次调用 logProps 时,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent);

这样做会产生一些不良后果,其一是输入组件再也无法像HOC增强之前那样使用了,更严重的是,如果你再用另一个同样会修改componentDidUpdateHOC增强它,那么前面的HOC就会失效,同时这个HOC也无法应用于没有生命周期的函数组件。
修改传入组件的HOC是一种糟糕的抽象方式,调用者必须知道他们是如何实现的,以避免与其他HOC发生冲突。HOC不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能。

function logProps(WrappedComponent) {return class extends React.Component {componentDidUpdate(prevProps) {console.log("Current props: ", this.props);console.log("Previous props: ", prevProps);}render() {// 将 input 组件包装在容器中,而不对其进行修改,Nice!return <WrappedComponent {...this.props} />;}}
}

过滤props

HOC为组件添加特性,自身不应该大幅改变约定,HOC返回的组件与原组件应保持类似的接口。HOC应该透传与自身无关的props,大多数HOC都应该包含一个类似于下面的render方法。

render() {// 过滤掉额外的 props,且不要进行透传const { extraProp, ...passThroughProps } = this.props;// 将 props 注入到被包装的组件中。// 通常为 state 的值或者实例方法。const injectedProp = someStateOrInstanceMethod;// 将 props 传递给被包装组件return (<WrappedComponentinjectedProp={injectedProp}{...passThroughProps}/>);
}

最大化可组合性

并不是所有的HOC都一样,有时候它仅接受一个参数,也就是被包裹的组件。

const NavbarWithRouter = withRouter(Navbar);

HOC通常可以接收多个参数,比如在RelayHOC额外接收了一个配置对象用于指定组件的数据依赖。

const CommentWithRelay = Relay.createContainer(Comment, config);

最常见的HOC签名如下,connect是一个返回高阶组件的高阶函数。

// React Redux 的 `connect` 函数
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);

这种形式可能看起来令人困惑或不必要,但它有一个有用的属性,像connect函数返回的单参数HOC具有签名Component => Component,输出类型与输入类型相同的函数很容易组合在一起。同样的属性也允许connect和其他HOC承担装饰器的角色。此外许多第三方库都提供了compose工具函数,包括lodashReduxRamda

const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))// 你可以编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(// 这些都是单参数的 HOCwithRouter,connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)

不要在render方法中使用HOC

Reactdiff算法使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树,如果从render返回的组件与前一个渲染中的组件相同===,则React通过将子树与新子树进行区分来递归更新子树,如果它们不相等,则完全卸载前一个子树。
通常在使用的时候不需要考虑这点,但对HOC来说这一点很重要,因为这代表着你不应在组件的render方法中对一个组件应用HOC

render() {// 每次调用 render 函数都会创建一个新的 EnhancedComponent// EnhancedComponent1 !== EnhancedComponent2const EnhancedComponent = enhance(MyComponent);// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!return <EnhancedComponent />;
}

这不仅仅是性能问题,重新挂载组件会导致该组件及其所有子组件的状态丢失,如果在组件之外创建HOC,这样一来组件只会创建一次。因此每次render时都会是同一个组件,一般来说,这跟你的预期表现是一致的。在极少数情况下,你需要动态调用HOC,你可以在组件的生命周期方法或其构造函数中进行调用。

务必复制静态方法

有时在React组件上定义静态方法很有用,例如Relay容器暴露了一个静态方法getFragment以方便组合GraphQL片段。但是当你将HOC应用于组件时,原始组件将使用容器组件进行包装,这意味着新组件没有原始组件的任何静态方法。

// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === "undefined" // true

为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上。

function enhance(WrappedComponent) {class Enhance extends React.Component {/*...*/}// 必须准确知道应该拷贝哪些方法 :(Enhance.staticMethod = WrappedComponent.staticMethod;return Enhance;
}

但要这样做,你需要知道哪些方法应该被拷贝,你可以使用hoist-non-react-statics依赖自动拷贝所有非React静态方法。

import hoistNonReactStatic from "hoist-non-react-statics";
function enhance(WrappedComponent) {class Enhance extends React.Component {/*...*/}hoistNonReactStatic(Enhance, WrappedComponent);return Enhance;
}

除了导出组件,另一个可行的方案是再额外导出这个静态方法。

// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;// ...单独导出该方法...
export { someFunction };// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from "./MyComponent.js";

Refs不会被传递

虽然高阶组件的约定是将所有props传递给被包装组件,但这对于refs并不适用,那是因为ref实际上并不是一个prop,就像key一样,它是由React专门处理的。如果将ref添加到HOC的返回组件中,则ref引用指向容器组件,而不是被包装组件,这个问题可以通过React.forwardRef这个API明确地将refs转发到内部的组件。。

function logProps(Component) {class LogProps extends React.Component {componentDidUpdate(prevProps) {console.log('old props:', prevProps);console.log('new props:', this.props);}render() {const {forwardedRef, ...rest} = this.props;// 将自定义的 prop 属性 “forwardedRef” 定义为 refreturn <Component ref={forwardedRef} {...rest} />;}}// 注意 React.forwardRef 回调的第二个参数 “ref”。// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”// 然后它就可以被挂载到被 LogProps 包裹的子组件上。return React.forwardRef((props, ref) => {return <LogProps {...props} forwardedRef={ref} />;});
}

示例

<!DOCTYPE html>
<html><head><meta charset="UTF-8" /><title>React</title>
</head><body><div id="root"></div>
</body>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">class WrappedComponent extends React.Component {render() {return <input name="name" />;}}const HOC = (WrappedComponent) => {return class EnhancedComponent extends React.Component {constructor(props) {super(props);this.state = { name: "" };}render() {const newProps = {value: this.state.name,onChange: e => this.setState({name: e.target.value}),}return <WrappedComponent {...this.props} {...newProps} />;}}}const EnhancedComponent = HOC(WrappedComponent);const HOC2 = (WrappedComponent) => {return class EnhancedComponent extends WrappedComponent {render() {return this.props.isRender && super.render();  }}}const EnhancedComponent2 = HOC2(WrappedComponent);var vm = ReactDOM.render(<><EnhancedComponent /><EnhancedComponent2 isRender={true} /></>,document.getElementById("root"));
</script></html>

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://juejin.cn/post/6844903477798256647
https://juejin.cn/post/6844904050236850184
https://zh-hans.reactjs.org/docs/higher-order-components.htm

React中的高阶组件相关推荐

  1. React 中的高阶组件及其应用场景

    本文目录 什么是高阶组件 React 中的高阶组件 属性代理(Props Proxy) 反向继承(Inheritance Inversion) 高阶组件存在的问题 高阶组件的约定 高阶组件的应用场景 ...

  2. 理解react中的高阶组件和高阶函数

    高阶组件 ES6中使用关键字class来使它的语法有和 类 相似的写法和功能,但是它的本质还是一个函数. 因此,高阶组件本质上是一个函数,只是这种组件(函数)接收一个组件,返回一个新的组件. 比如下面 ...

  3. React中自定义高阶组件的应用(HOC)

    什么是HOC? HOC(Higher-order component)是一种React 的进阶使用方法,只要还是为了便于组件的复用.强调一点,HOC本身并不是 React API, 它就是一个方法,一 ...

  4. axios请求拦截器错误_React中使用高阶组件和axios的拦截器,统一处理请求失败提示...

    在前端开发中,判断边界条件和重要,通常我们要花费开发中的很大一部分时间做边界条件处理.发送ajax请求时,假设有这样一个需求: 每个页面发送ajax请求,如果请求失败,在页面上统一弹出样式一样的错误提 ...

  5. 16、react 中的高阶函数和柯里化

    react 中的高阶函数和柯里化 这一篇博文我们说一下 高阶函数 和 柯里化,这两个次可能第一次听说,不知道是啥意思,我们先不管他哈,记得上一篇博客,我们实现了一个登陆的案例是吧?输入用户名和密码,点 ...

  6. React之HOC(高阶组件)

    一.概念 Higher-Order Components就是一个函数,传给它一个组件,它返回一个新的组件.就相当于手机壳,通过包装组件,增强组件功能,抽离公共逻辑. 二.步骤 2.1 创建一个函数 i ...

  7. react学习17-React高阶组件

    高阶组件 高阶组件 HOC(hight ordered component) withRouter就是高阶组件 高阶组件的本质其实就是一个函数,该函数参数接收一个组件,返回值依然是一个组件,返回的组件 ...

  8. react——props和高阶组件

    目录 前言 一.render props 模式 1. react复用概述 2. render props 模式 3. children 代替 render 属性 二.高阶组件 1. 概述 2. 使用步 ...

  9. react进阶系列 - 高阶组件详解四:高阶组件的嵌套使用

    前面有讲到过很多页面会在初始时验证登录状态与用户角色.我们可以使用高阶组件来封装这部分验证逻辑.封装好之后我们在使用的时候就可以如下: export default withRule(Home); 但 ...

最新文章

  1. 利用Python基础代码语句,实现2G时代文字小游戏,世界如此简单
  2. 模型越大,真的越香吗?千亿参数之后是万亿,万亿之后呢?
  3. 表面积最小(POJ3536)
  4. HDU 1181 变形课
  5. 【开发环境】Ubuntu 中使用 VSCode 开发 C/C++ ④ ( 创建 tasks.json 编译器构建配置文件 | tasks.json 编译器构建配置文件分析 )
  6. js 获取vue组件html_vue.js中怎么引入组件?
  7. linux下的X server:linux图形界面原理
  8. [WPF系列]Adorner应用-自定义控件ImageHotSpot
  9. epel源mysql版本_centos网络yum源和epel源(2017可用首选)
  10. Intel i5-7200U (3100MHZ),1*8GB(DDR3 1600) 在 Aida64 V5.97.4600 的测试结果
  11. php钱兆楼,PHP动态网站开发实例教程(第2版)
  12. linux垃圾文件清理,请问 如何linux 垃圾文件清理、
  13. 计算机的新兴技术在测绘工程领域的应用,测绘新技术在测绘工程测量中应用探讨.doc...
  14. 打开excel表格会自动打开一个空表格
  15. R语言学习笔记:主成分分析及因子分析
  16. hadoop实战(三) 使用HDFS操作文件
  17. 一些概念名称的来历札记
  18. 统一用户登录管理认证LDAP 服务端部署
  19. 可以免费发布本地生活分类信息的网站大全
  20. Decimal是什么类型

热门文章

  1. 通过qemu调试centos7内核
  2. 详解Go语言的计时器
  3. 关于redis的持久化
  4. 软件架构自学笔记---架构分析
  5. 利用TICK搭建Docker容器可视化监控中心
  6. 服务端架构设计及功能说明-续1
  7. KeyMob具有手机广告优化的管理平台
  8. Seafile 1.3 发布,文件同步和协作平台
  9. 虚拟机网卡无法启动获取ip地址
  10. 过滤器链模式PK匿名方法实现,哪个更优雅?