react 的高阶组件浅析
react 中 Hooks 浅析
react 的 mixins、hoc、hooks 对比
简介
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
高阶组件可以看作 React 对装饰模式的一种实现,高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
语法
1 | const EnhancedComponent = higherOrderComponent(WrappedComponent); |
可以从下面几个方面探索 HOC。
HOC 的实现方式
- 属性代理
- 反向继承
高阶组件的使用场景
- 操作 props(属性)
- 通过 Refs 访问到组件实例
- 组件状态提升
- 操作 state
- 渲染劫持
- 用其他元素包裹 WrappedComponent
HOC 的实现方式
React
中实现 HOC
的两种主要方式:Props Proxy
(属性代理)(PP)和 Inheritance Inversion
(反向继承)(II) 。 两者都支持不同的方式来操作 WrappedComponent 。
这里不会详细介绍生命周期方法来做什么,因为它不是 HOC
的特性,而是 React
的特性。 但请注意,使用 Inheritance Inversion(反向继承)
,您可以为 WrappedComponent
创建新的生命周期方法。 记得总是这样调用 super[lifecycleHook]
,这样就不会破坏 WrappedComponent
。
属性代理 Props Proxy
Props Proxy(属性代理)(PP) 以下列方式实现:
1 | function ppHOC(WrappedComponent) { |
这里的重要部分是 HOC 得 render 方法返回WrappedComponent 类型的 React 元素。我们还通过 HOC 接收到得props
(属性),这就是名字Props Proxy
的由来。
对比原生组件增强的项:
- 操作 props
- 可操作组件的生命周期
- 通过 Refs 访问到组件实例
- 用其他元素包裹 WrappedComponent
- 提取 state(状态)
Inheritance Inversion(反向继承)
Inheritance Inversion(反向继承)(II) 通过以下方式实现:
1 | function iiHOC(WrappedComponent) { |
返回的 HOC 类Enhancer 继承(extends)了WrappedComponent。它被成为Inheritance Inversion(反向继承),因为它不是用 WrappedComponent 来继承某些Enhancer类。而是被Enhancer被动继承。通过这种方式,他们之间的关系似乎是反向(inverse)。
反向继承允许 HOC 通过 this 访问 WrappedComponent 实例,这意味着它可以访问 state(状态),props(属性),组件生命周期方法和 render 方法。
对比原生组件增强的项:
- 可操作所有传入的 props
- 可操作组件的生命周期
- 获取 refs
- 可操作 state
- 可以渲染劫持
高阶组件的使用场景
操作 props(属性)
通过属性代理实现
你可以读取、添加、编辑、删除
传给 WrappedComponent
的 props(属性)
。在删除或编辑
重要的 props(属性)
时要小心,你应该通过命名空间确保高阶组件的 props
不会破坏 WrappedComponent
。
1 | function ppHOC(WrappedComponent) { |
通过 Refs 访问到组件实例
高阶组件
中可获取原组件的ref
,通过ref
获取组件实例
,如下面的代码,当程序初始化完成后调用原组件的 log 方法。
通过属性代理实现
1 | function refHOC(WrappedComponent) { |
调用高阶组件的时候并不能获取到原组件的真实 ref,需要手动进行传递,详情请看
组件状态提升
将原组件的状态提取到HOC
中进行管理,如下面的代码,我们将Input
的value
提取到HOC
中进行管理,使它变成受控组件,同时不影响它使用onChange
方法进行一些其他操作。基于这种方式,我们可以实现一个简单的双向绑定
。
通过属性代理实现
示例:在以下提取 state(状态)示例中,我们非常规的提取 name 输入字段的值和 onChange 处理程序。代码如下:
1 | function ppHOC(WrappedComponent) { |
可以像这样使用它:
1 | @ppHOC //装饰器 |
操作 state
HOC
可以读取,编辑和删除 WrappedComponent
实例的状态,如果需要,还可以添加更多的 state(状态)
。 请记住,您正在弄乱 WrappedComponent
的 state(状态)
,这会导致您破坏一些东西。 大多数情况下,HOC
应限于读取或添加 state(状态)
,而添加 state(状态)
时应该被命名为不会弄乱 WrappedComponent
的 state(状态)
。
通过反向继承实现
示例:通过访问 WrappedComponent
的 props(属性)
和 state(状态)
进行调试
1 | export function IIHOCDEBUGGER(WrappedComponent) { |
这个 HOC
用其他元素包裹着 WrappedComponent
,并且还显示了 WrappedComponent
的实例 props(属性)
和 state(状态)
。
渲染劫持(Render Highjacking)
通过反向继承实现
它被称为 渲染劫持(Render Highjacking),因为 HOC 控制了 WrappedComponent 的渲染输出,并且可以用它做各种各样的事情。
在渲染劫持中,您可以:state(状态),props(属性)
- 读取,添加,编辑,删除渲染输出的任何 React 元素中的 props(属性)
- 读取并修改 render 输出的 React 元素树
- 有条件地渲染元素树
- 把样式包裹进元素树(就像在 Props Proxy(属性代理) 中的那样)
注:render 是指 WrappedComponent.render 方法
通过渲染劫持可以做到组合渲染和条件渲染.
示例 1:条件渲染。除非this.props.loggedIn
不为true
,否则此HOC
将准确渲染WrappedComponent
将渲染的内容。(假设 HOC 将收到 loggedIn props(属性)).
1 | // 通过反向继承实现 |
示例 2:组合渲染。 新增一个 title。
1 | // 通过反向继承实现 |
注:在
Props Proxy
(属性代理) 类型的高阶函数中做不到渲染劫持。
虽然可以通过 WrappedComponent.prototype.render
访问 render
方法,但是您需要模拟 WrappedComponent
实例及其 props
(属性),并且可能需要自己处理组件生命周期,而不是依赖 React
执行它。 在我的实验中不值得这么做,如果你想做渲染劫持(Render Highjacking
),你应该使用 Inheritance Inversion
(反向继承) 而不是 Props Proxy
(属性代理)。 请记住,React 在内部处理组件实例,而处理实例的唯一方法是通过 this 或 refs 。
用其他元素包裹 WrappedComponent
通过属性代理实现
可以将 WrappedComponent 与其他组件和元素包装在一起,以用于样式,布局或其他目的。 一些基本用法可以通过常规父组件来完成.
1 | function ppHOC(WrappedComponent) { |
命名
使用 HOC
包裹组件时,会丢失原始 WrappedComponent
的名称,这可能会在开发和调试时影响到您。人们通常做的是通过获取 WrappedComponent
的名称并预先添加某些内容来自定义 HOC
的名称。 以下内容摘自 React-Redux
。用 HOC
包裹了一个组件会使它失去原本 WrappedComponent
的名字,可能会影响开发和调试。通常会用 WrappedComponent
的名字加上一些 前缀作为 HOC 的名字。下面的代码来自 React-Redux:
1 | function getDisplayName(WrappedComponent) { |
如何使用 HOC
上面的示例代码都写的是如何声明一个HOC
,HOC
实际上是一个函数,所以我们将要增强的组件作为参数调用HOC
函数,得到增强后的组件。
1 | class myComponent extends Component { |
compose
假设现在我们有 logger,visible,style 等多个 HOC,现在要同时增强一个 Input 组件:
1 | logger(visible(style(Input))); |
这种代码非常的难以阅读,我们可以手动封装一个简单的函数组合工具,将写法改写如下:
1 | const compose = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args))); |
compose
函数返回一个所有函数组合后的函数,compose(f, g, h)
和 (...args) => f(g(h(...args)))
是一样的。
很多第三方库都提供了类似compose
的函数,例如lodash.flowRight
,Redux
提供的combineReducers
函数等。
Decorators
我们还可以借助 ES7 为我们提供的 Decorators 来让我们的写法变的更加优雅:
1 | @logger |
Decorators
是ES7
的一个提案,还没有被标准化,但目前Babel
转码器已经支持,我们需要提前配置babel-plugin-transform-decorators-legacy
:
1 | "plugins": ["transform-decorators-legacy"] |
HOC 的实际应用
多个组件拥有类似的逻辑,我们要对重复的逻辑进行复用, 官方文档中CommentList的示例也是解决了代码复用问题。
日志打点
某些页面需要记录用户行为,性能指标等等,通过高阶组件做这些事情可以省去很多重复代码。
1 | function logHoc(WrappedComponent) { |
可用、权限控制
1 | function auth(WrappedComponent) { |
authList
是我们在进入程序时向后端请求的所有权限列表,当组件所需要的权限不列表中,或者设置的 visible 是 false,我们将其显示为传入的组件样式,或者null
。我们可以将任何需要进行权限校验的组件应用HOC
:
1 | @auth |
双向绑定
在vue
中,绑定一个变量后可实现双向数据绑定,即表单中的值改变后绑定的变量也会自动改变。而React
中没有做这样的处理,在默认情况下,表单元素都是非受控组件。给表单元素绑定一个状态后,往往需要手动书写onChange
方法来将其改写为受控组件,在表单元素非常多的情况下这些重复操作是非常痛苦的。
我们可以借助高阶组件来实现一个简单的双向绑定,代码略长,可以结合下面的思维导图进行理解。
…代码未完成(后续补上)
表单校验
基于上面的双向绑定的例子,我们再来一个表单验证器,表单验证器可以包含验证函数以及提示信息,当验证不通过时,展示错误信息:
…代码未完成(后续补上)
使用 HOC 的注意事项
静态属性拷贝
当我们应用HOC
去增强另一个组件时,我们实际使用的组件已经不是原组件了,所以我们拿不到原组件的任何静态属性,我们可以在HOC
的结尾手动拷贝他们:
1 | function proxyHOC(WrappedComponent) { |
如果原组件有非常多的静态属性,这个过程是非常痛苦的,而且你需要去了解需要增强的所有组件的静态属性是什么,我们可以使用hoist-non-react-statics
来帮助我们解决这个问题,它可以自动帮我们拷贝所有非 React 的静态方法,使用方式如下:
1 | import hoistNonReactStatic from 'hoist-non-react-statics'; |
传递 refs
使用高阶组件后,获取到的ref
实际上是最外层
的容器组件
,而非原组件
,但是很多情况下我们需要用到原组件的ref
。
高阶组件并不能像透传 props 那样将 refs 透传,我们可以用一个回调函数来完成 ref 的传递:
1 | function hoc(WrappedComponent) { |
React 16.3
版本提供了一个forwardRef API
来帮助我们进行refs
传递,这样我们在高阶组件上获取的ref
就是原组件的 ref 了,而不需要再手动传递,如果你的React
版本大于16.3
,可以使用下面的方式:
1 | function hoc(WrappedComponent) { |
不要在 render 方法中使用 HOC
React Diff 算法的原则是:
- 使用组件标识确定是卸载还是更新组件
- 如果组件的和前一次渲染时标识是相同的,递归更新子组件
- 如果标识不同卸载组件重新挂载新组件
每次调用高阶组件生成的都是是一个全新的组件,组件的唯一标识响应的也会改变,如果在 render 方法调用了高阶组件,这会导致组件每次都会被卸载后重新挂载。
不要改变原始组件
官方文档对高阶组件的说明:
高阶组件就是一个没有副作用的纯函数。
我们再来看看纯函数的定义:
如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数。 该函数不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变。
我们使用高阶组件是为了增强而非改变原组件。
透传不相关的 props
使用高阶组件,我们可以代理所有的props
,但往往特定的 HOC 只会用到其中的一个或几个props
。我们需要把其他不相关的props
透传给原组件,如下面的代码:
1 | function visible(WrappedComponent) { |
我们只使用visible
属性来控制组件的显示可隐藏,把其他props
。
HOC 的缺陷
HOC
需要在原组件上进行包裹或者嵌套,如果大量使用HOC
,将会产生非常多的嵌套,这让调试变得非常困难。HOC
可以劫持props
,在不遵守约定的情况下也可能造成冲突。
总结
HOC 相对于 Mixins 的好处:
- 高阶组件就是一个没有副作用的纯函数,各个高阶组件不会互相依赖耦合
- 高阶组件也有可能造成冲突,但我们可以在遵守约定的情况下避免这些行为
- 高阶组件并不关心数据使用的方式和原因,而被包裹的组件也不关心数据来自何处。高阶组件的增加不会为原组件增加负担
HOC 实现方式有两种:
- 通过属性代理实现
- 通过反向继承实现
高阶组件的使用场景:
- 操作 props(属性)(通过属性代理实现、通过反向继承实现)
- 通过 Refs 访问到组件实例 (通过属性代理实现)
- 组件状态提升(通过属性代理实现)
- 操作 state (通过反向继承实现)
- 渲染劫持(通过属性代理实现、通过反向继承实现)
- 用其他元素包裹 WrappedComponent (通过属性代理实现)
参考
高阶组件
深入理解 React 高阶组件(Higher Order Component,简称:HOC)
【React 深入】从 Mixin 到 HOC 再到 Hook