React 高阶组件浅析
背景
高阶组件的这种写法的诞生来自于社区的实践,目的是解决一些交叉问题(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
技术来解决此类问题所带来的困扰远高于其本身的价值。更多资料可以查阅官方的说明。
高阶函数的定义
说到高阶组件,就不得不先简单的介绍一下高阶函数。下面展示一个最简单的高阶函数
1 | const add = (x,y,f) => f(x)+f(y) |
当我们调用add(-5, 6, Math.abs)
时,参数 x,y 和f 分别接收 -5,6 和 Math.abs
,根据函数定义,我们可以推导计算过程为:
1 | x ==> -5 |
用代码验证一下:
1 | add(-5, 6, Math.abs); //11 |
高阶在维基百科的定义如下
高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
高阶组件的定义
那么,什么是高阶组件呢?类比高阶函数的定义,高阶组件就是接受一个组件作为参数并返回一个新组件的函数。这里需要注意高阶组件是一个函数,并不是组件,这一点一定要注意。
同时这里强调一点高阶组件本身并不是 React
API。它只是一种模式,这种模式是由 React
自身的组合性质必然产生的。
更加通俗的讲,高阶组件通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的 React 组件,供其他组件调用。
一个简单的高阶组件
下面我们来实现一个简单的高阶组件
1 | export default WrappedComponent => class HOC extends Component { |
在其他组件中,我们引用这个高阶组件来强化它
1 | export default class Demo extends Component { |
下面我们来看一下React DOM Tree
,调用了高阶组件之后,发生了什么:
可以看到,Demo
被 HOC
包裹(wrapped)了之后添加了一个标题默认标题。但是同样会发现,如果调用了多个 HOC
之后,我们会看到很多的HOC
,所以应
该做一些优化,也就是在高阶组件包裹(wrapped)以后,应该保留原有的名称。
我们改写一下上述的高阶组件代码,增加一个 getDisplayName
函数,之后为Demo
添加一个静态属性 displayName
。
1 | const getDisplayName = component => component.displayName || component.name || 'Component'; |
再次观察React DOM Tree
可以看到,该组件原本的名称已经显示在React DOM Tree
上了。
这个HOC 的功能是为原有的组件添加一个标题,也就是说所有需要添加标题的组件都可以通过调用此 HOC 进行包裹(wrapped) 后实现此功能。
为高阶组件传参
现在,我们的 HOC
已经可以为其他任意组件提供标题了,但是我们还希望可以修改标题中的字段。由于我们的高阶组件是一个函数,所以可以为其添加一个参数title
。下面我们对HOC
进行改写:
1 | export default (WrappedComponent, title = '默认标题') => class HOC extends Component { |
之后我们进行调用:
1 | const WithHeaderDemo = withHeader(Demo,'高阶组件添加标题'); |
此时观察
React DOM Tree
。可以看到,标题已经正确的进行了设置。
当然我们也可以对其进行柯里化:
1 | export default (title = '默认标题') => WrappedComponent => class HOC extends Component { |
常见的HOC 实现方式
基于属性代理(Props Proxy)的方式
属性代理是最常见的高阶组件的使用方式,上面所说的高阶组件就是这种方式。
它通过做一些操作,将被包裹组件的props
和新生成的props
一起传递给此组件,这称之为属性代理。
1 | export default function GenerateId(WrappedComponent) { |
调用
GenerateId
:1 | const PropsBorkerDemo = GenerateId(Demo); |
之后我们观察
React Dom Tree
:可以看到我们通过
GenerateId
顺利的为 Demo
添加了 id
。基于反向继承(Inheritance Inversion)的方式
首先来看一个简单的反向继承的例子:
1 | export default function (WrappedComponent) { |
如你所见返回的高阶组件类(
Enhancer
)继承了 WrappedComponent
。而之所以被称为反向继承是因为 WrappedComponent
被动地被 Enhancer
继承,而不是
WrappedComponent
去继承 Enhancer
。通过这种方式他们之间的关系倒转了。反向继承允许高阶组件通过 this
关键词获取 WrappedComponent
,意味着它可以获取到 state
,props
,组件生命周期(Component Lifecycle)钩子,以及渲染方法(render)。深入了解可以阅读@Wenliang文章中Inheritance Inversion(II)
这一节的内容。
使用高阶组件遇到的问题
静态方法丢失
当使用高阶组件包装组件,原始组件被容器组件包裹,也就意味着新组件会丢失原始组件的所有静态方法。
下面为 Demo 添加一个静态方法:
1 | Demo.getDisplayName = () => 'Demo'; |
之后调用
HOC
:1 | // 使用高阶组件 |
解决这个问题最简单(Yǘ Chǚn)的方法就是,将原始组件的所有静态方法全部拷贝给新组件:
1 | export default (title = '默认标题') => (WrappedComponent) => { |
这样做,就需要你清楚的知道都有哪些静态方法需要拷贝的。或者你也可是使用hoist-non-react-statics来帮你自动处理,它会自动拷贝所有非React的静态方法:
1 | import hoistNonReactStatic from 'hoist-non-react-statics'; |
Refs属性不能传递
一般来说,高阶组件可以传递所有的props属性给包裹的组件,但是不能传递 refs
引用。因为并不是像 key
一样,refs
是一个伪属性,React
对它进行了特殊处理。
如果你向一个由高级组件创建的组件的元素添加 ref
应用,那么 ref
指向的是最外层容器组件实例的,而不是包裹组件。
但有的时候,我们不可避免要使用 refs
,官方给出的解决方案是:
传递一个ref回调函数属性,也就是给ref应用一个不同的名字
同时还强调道:React在任何时候都不建议使用 ref应用
改写 Demo
1 | class Demo extends Component { |
之后我们进行调用:
1 | <WithHeaderDemo |
虽然这并不是最完美的解决方案,但是
React
官方说他们正在探索解决这个问题的方法,能够让我们安心的使用高阶组件而不必关注这个问题。结语
这篇文章只是简单的介绍了高阶组件的两种最常见的使用方式:属性代理
和反向继承
。以及高阶组件的常见问题。希望通过本文的阅读使你对高阶组件有一个基本的认识。
写本文所产生的代码在study-hoc中。
本文作者:杨过
本文同步发表于:HYPERS 前端博客
参考文章:
Higher-Order Components
深入浅出React高阶组件
带着三个问题一起深入浅出React高阶组件
阮一峰 - 高阶函数
深入理解高阶组件