React文档:高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。 HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 总结来说就是两点:
什么是高阶组件
官方说,高阶组件是参数为组件,返回值为新组件的函数。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
通俗的定义:高阶组件是函数,通过包裹(wrapped)被传入的React组件,经过一系列处理,最终返回一个相对增强(enhanced)的React组件,供其他组件调用。
另一个相似概念:高阶函数
Higher-Order 这个单词相信都很熟悉,Higher-Order function(高阶函数)在函数式编程是一个基本概念,它描述的是这样一种函数,接受函数作为输入,或是输出一个函数。比如常用的工具方法 map、reduce、sort 都是高阶函数。
为什么要用高阶组件
React 的文档说的非常清楚, 高阶组件是一种用于复用组件逻辑模式。总结来说就是两点:
- 逻辑复用. 把一些通用的代码逻辑提取出来放到高阶组件中, 让更多组件可以共享
- 分离关注点. 在之前的章节中提到"逻辑和视图分离"的原则. 高阶组件可以作为实现该原则的载体. 我们一般将行为层或者业务层抽取到高阶组件中来实现, 让展示组件只关注于 UI
与父组件区别
高阶组件作为一个函数,它可以更加纯粹地关注业务逻辑层面的代码,比如数据处理,数据校验,发送请求等,可以改善目前代码里业务逻辑和UI逻辑混杂在一起的现状。父组件则是UI层的东西,我们先前经常把一些业务逻辑处理放在父组件里,这样会造成父组件混乱的情况。为了代码进一步解耦,可以考虑使用高阶组件这种模式。
高阶组件的用法
-
属性代理(Props Proxy): 代理传递给被包装组件的 props, 对 props 进行操作. 这种方式用得最多. 使用这种方式可以做到:
- 操作 props
- 通过refs访问被包装组件实例
- 提取 state
- 用其他元素包裹WrappedComponent
-
反向继承(Inheritance Inversion): 高阶组件继承被包装的组件. 可以实现:
- 渲染劫持: 即控制被包装组件的渲染输出.
- 操作 state: state 一般属于组件的内部细节, 通过继承的方式可以暴露给子类. 可以增删查改被包装组件的 state, 除非你知道你在干什么, 一般不建议这么做.
属性代理例子:
export default function withHeader(WrappedComponent) {
return class HOC extends Component {
render() {
const newProps = {
test:'hoc'
}
// 透传props,并且传递新的newProps
return <div>
<WrappedComponent {...this.props} {...newProps}/>
</div>
}
}
}
反向继承例子:
export default function (WrappedComponent) {
return class Inheritance extends WrappedComponent {
componentDidMount() {
// 可以方便地得到state,做一些更深入的修改。
console.log(this.state);
}
render() {
return super.render();
}
}
}
总结一下高阶组件的应用场景:
- 操作 props: 增删查改 props. 例如转换 props, 扩展 props, 固定 props, 重命名 props
- 依赖注入. 注入 context 或外部状态和逻辑, 例如 redux 的 connnect, react-router 的 withRouter. 旧 context 是实验性 API, 所以很多库都不会将 context 保留出来, 而是通过高阶组件形式进行注入
- 扩展 state: 例如给函数式组件注入状态
- 避免重复渲染: 例如 React.memo
- 分离逻辑, 让组件保持 dumb
高阶组件的示例
- withRouter 方法
- Redux 的 connect方法
- ant design 表单的Form.create()方法
- 路由权限控制, 同一个路由地址,根据不同的用户角色或者登陆状态, 展示不同的UI视图
- 当你想要给很多组件赋予新的props时
简而言之,如果你需要给很多组件都写相同的判断逻辑,那么可以考虑提取出一个高阶组件
实现一个高阶组件
下面我们来实现一个最简单的高阶组件(函数),它接受一个React组件,包裹后然后返回。
export default function withHeader(WrappedComponent) {
return class HOC extends Component {
render() {
return <div>
<div className="demo-header">
我是标题
</div>
<WrappedComponent {...this.props}/>
</div>
}
}
}
在其他组件里,我们引用这个高阶组件,用来强化它。
@withHeader
export default class Demo extends Component {
render() {
return (
<div>
我是一个普通组件
</div>
);
}
}
在这里使用了ES7里的decorator,来提升写法上的优雅,但是实际上它只是一个语法糖,下面这种写法也是可以的。
const EnhanceDemo = withHeader(Demo);
组合多个高阶组件
上述高阶组件为React组件增强了一个功能,如果需要同时增加多个功能需要怎么做?这种场景非常常见,例如我既需要增加一个组件标题,又需要在此组件未加载完成时显示Loading。
@withHeader
@withLoading
class Demo extends Component{
}
使用compose可以简化上述过程,也能体现函数式编程的思想。
const enhance = compose(withHeader,withLoading);
@enhance
class Demo extends Component{
}
调试规范
但是随之带来的问题是,如果这个高阶组件被使用了多次,那么在调试的时候,会显示在 React Developer Tools 中,将会看到一大堆HOC,所以这个时候需要做一点小优化,就是在高阶组件包裹后,应当保留其原有名称。
我们改写一下上述的高阶组件代码,增加了getDisplayName函数以及静态属性displayName,此时再去观察DOM Tree。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function getDisplayName(component) {
return component.displayName || component.name || 'Component';
}
function withHeader(WrappedComponent) {
return class HOC extends React.Component {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
render() {
return <div>
<div className="demo-header">我是标题</div>
<WrappedComponent {...this.props}/>
</div>
}
}
}
class Demo extends React.Component {
render() {
return (
<div>我是一个普通组件</div>
);
}
}
const EnhanceDemo = withHeader(Demo)
class MyComponent extends React.Component{
render(){
return <EnhanceDemo/>
}
}
ReactDOM.render(
<MyComponent />,
document.getElementById('root')
);
此时,原本组件的名称正确地显示在了DOM Tree上。
MyComponent
HOC(Demo)
<div>
<div className="demo-header">我是标题</div>
<Demo>
<div>我是一个普通组件</div>
</Demo>
HOC(Demo)
MyComponent
由此可以看出,高阶组件的主要功能是封装并抽离组件的通用逻辑,让此部分逻辑在组件间更好地被复用。
高阶组件实战
实现一个loading组件
实现Loading组件时,发现需要去拦截它的渲染过程,故使用了反向继承的方式来完成。
在通过装饰器调用时,需要传入一个函数作为入参,函数可以获取到props,随后返回一个Boolean对象,来决定组件是否需要显示Loading态
import React, {Component} from 'react';
import {Spin} from 'antd';
export default function (loadingCheck) {
return function (WrappedComponent) {
return class extends WrappedComponent {
componentWillUpdate(nextProps, nextState) {
console.log('withLoading将会更新');
}
render() {
if (loadingCheck(this.props)) {
return <Spin tip="加载中" size="large">
{super.render()}
</Spin>
} else {
return super.render();
}
}
}
}
}
// 使用
@withLoading(props => {
return props.IndexStore.accountList.length == 0;
})
实现一个copy组件
实现copy组件的时候,我们发现不需要去改变组件内部的展示方式,只是为其在外围增加一个功能,并不会侵入被传入的组件,故使用了属性代理的方式。
import gotem from 'gotem';
import React, {Component} from 'react';
import ReactDom from 'react-dom';
import {message} from 'antd';
export default copy = (targetName) => {
return (WrappedComponent) => {
return class extends Component {
componentDidMount() {
const ctx = this;
const dom = ReactDom.findDOMNode(ctx);
const nodes = {
trigger: dom,
// targetName为DOM选择器,复制组件将会复制它的值
target: dom.querySelector(targetName)
};
// 当trigger被点击时就会把target下文本内容拷贝到剪切板上
gotem(nodes.trigger, nodes.target, {
success: function () {
message.success('复制成功');
},
error: function () {
message.error('复制失败,请手动输入');
}
});
}
render() {
return <WrappedComponent {...this.props}/>;
}
};
};
}
// 使用
// 传入 h3 ,让复制组件去获取它的值
@copy('h3')
class Info extends Component {
render() {
return (
<div>
<h3>
阿里云,点击复制这段文字
</h3>
</div>
);
}
}
总结
高阶组件是Decorator模式在React的一种实现,它可以抽离公共逻辑,像洋葱一样层层叠加给组件,每一层职能分明,可以方便地抽离与增添。在优化代码或解耦组件时,可以考虑使用高阶组件模式。
参考文章
官网高阶组件
React 高阶组件
深入理解 React 高阶组件
React组件设计实践总结04 - 组件的思维
关于React的高阶组件
React高阶组件总结