Blog
Blog copied to clipboard
react 高阶组件
什么是高阶组件
高阶组件是react中的一个概念,实质来讲,高阶组件就是一个高阶函数
什么是高阶函数,就是满足以下任意一个条件:
-
可以传入另一个函数作为参数(比如filter、map)
-
可以返回一个函数(比如bind)
日常工作中,我们在不知不觉中就已经使用了高阶函数
const arr = [1,2,3,4,5,6];
const square = d => d ** 2;
arr.map(square) // [1, 4, 9, 16, 25, 36];
在这个例子中,我们的map函数通过传入的方法square实现了数组的二次方。 另一个我们经常会用到的例子:
<ul>
{list ? list.map((item, index) => {
return <li>{item.name}</li>
})}
</ul>
由这些高阶函数我们可以明白,其实高阶组件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式。具体地说,高阶组件就是:接收一个组件并返回另外一个新组件的函数,并且会继承传入组件(state,props,生命周期,render)。
用来做什么
简单来讲,提取重复代码,处理相同逻辑
高阶组件还有更多的功能:
- 操纵prop
// 操作props
// 可以对原组件的props进行增删改查,通常是查找和增加,删除和修改的话,需要考虑到`不能破坏原组件`。
function Hoc(WrappedComponent) {
return class extends React.Component {
render() {
const {userInfo,...otherprops} = this.props;
return <WrappedComponent {...otherprops} />
}
}
}
- 包装组件
// 将组件包裹起来,为了封装样式、布局等目的
function HOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return (
<div style={{display: 'block'}}>
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
- 操纵生命周期和state
// 操纵组件的方法
function HOC(WrappedComponent) {
return class ExampleEnhance extends WrappedComponent {
...
componentDidMount() {
super.componentDidMount();
}
componentWillUnmount() {
super.componentWillUnmount();
}
render() {
...
return super.render();
}
}
}
这里我们可以看到,我们的高阶组件继承了传入组件(一般的高阶组件是继承react的component),并且在render函数中return的是super.render(),从而实现了继承反转。因此高阶组件就可以获取到传入组件的生命周期钩子和方法。
这里举一个简单的例子: 我们先创建一个被包裹的组件:
import React, { Component } from "react";
export default class ComponentChild extends Component {
constructor(props) {
super(props);
this.state = {
message: "我是ComponentChild中的message"
};
}
componentDidMount() {
console.log("ComponentChild Did Mount");
}
clickComponent() {
console.log("ComponentChild click");
}
render() {
return <div>{this.state.message}</div>;
}
}
这里我们会返回一个信息,信息存在组件的state中,并且有一个clickComponent方法和一个生命周期方法,会在控制台打印信息。
接下来实现具有继承反转能力的高阶组件:
import React from "react";
//这样的方式,外部组件的 state 可以将被继承组件的 state 和 钩子函数彻底覆盖掉。同时,外部组件也可以调用被继承组件的方法。
const messageOtherHoc = WrappedComponent => {
return class extends WrappedComponent {
constructor(props) {
super(props);
this.state = {
message: "我是messageOtherHoc中的message"
};
}
componentDidMount() {
console.log("messageOtherHoc componentDidMount");
this.clickComponent();
}
render() {
return (
<div>
<div onClick={this.clickComponent}>messageOtherHoc 点击我</div>
<div>
<div>{super.render()}</div>
</div>
</div>
);
}
};
};
export default messageOtherHoc;
高阶组件中,我们会定义state,生命周期函数,并且在div中设置点击事件,调用this.clickComponent方法,显然,我们高阶组件中并没有写这样的方法;最后我们会返回传入组件的render函数,通过super找到传入组件
然后我们去调用这个高阶组件:
const Message2 = MessageOtherHoc(ComponentChild);
<Message2 />
打印台信息:

显而易见,高阶组件覆盖了传入组件的生命周期方法并且调用了传入组件的clickComponent方法。
这就意味着这样的高阶组件可以访问到WrappedComponent的state,props,生命周期和render方法。如果在高阶组件中定义了与WrappedComponent同名方法,将会发生覆盖,如果需要调用他们,我们就必须手动通过super进行调用。就像例子中那样。
如何创建一个高阶组件
一个简单易懂的例子,命名和代码可能不是规范的写法,但是可以说明问题 用于返回Tips相同的展示:
import React, { Component } from "react";
function withToolTipsSubscription(WrappedComponent) {
return class withToolTipsSubscription extends Component {
render() {
const { tipsType, ...otherProps } = this.props;
return (
<fieldset>
<legend>我是一个 {tipsType.type} 组件</legend>
<p>
<span>提示时间:{tipsType.date}</span>
</p>
<p>
<span>提示:</span>
</p>
<span>WrappedComponent is:</span>
<WrappedComponent {...otherProps} />
</fieldset>
);
}
};
}
export default withToolTipsSubscription;
我们的Tip可以拥有一个共同的标题,提示时间和一些文字
在这里面我们用到了操纵props,并把props继续传递给了我们的参数组件:
const { tipsType, ...otherProps } = this.props;
<WrappedComponent {...otherProps} />
AskToolTip.js
用于展示一个一个带有两个按钮的Tips
两个按钮可以和父组件进行交互,直接调用 this.props 调用父组件方法即可,无需其他操纵
import React, { Component } from "react";
import ToolTips from "./ToolTips";
class AskToolTips extends Component {
constructor(props) {
super(props);
this.handleConfirm = this.handleConfirm.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.confirmIndex = 0;
this.cancelIndex = 0;
}
handleConfirm() {
this.props.onConfirm((this.confirmIndex += 1));
}
handleCancel() {
this.props.onCancel((this.cancelIndex += 1));
}
render() {
return (
<div
style={{
borderWidth: "1px",
borderColor: "#d3d3d3",
borderStyle: "solid"
}}
>
<div>
<span>There are somthing will ask with you:</span>
<p>
<span>[ ConfirmTimes:{this.props.confirmTimes} ]</span>
<span>[ ConfirmTimes:{this.props.cancelTimes} ]</span>
</p>
</div>
<button onClick={this.handleConfirm}>confirm</button>
<button onClick={this.handleCancel}>cancel</button>
</div>
);
}
}
export default ToolTips(AskToolTips);
这个组件的写法和平常的组件大致无二,唯一的的区别在于 export 的内容
export default ToolTips(AskToolTips);
引入并返回我们包裹的高阶组件
ErrorToolTips.js
和上面的组件类似,但是这里只接收父组件传递的内容进行展示:
import React, { Component } from "react";
import ToolTips from "./ToolTips";
class ErrorToolTips extends Component {
render() {
return (
<div
style={{
borderWidth: "1px",
borderColor: "#d3d3d3",
borderStyle: "solid"
}}
>
<span>{this.props.msg.str}</span>
</div>
);
}
}
export default ToolTips(ErrorToolTips);
HocExample.js
组件的具体调用:
import React, { Component } from "react";
import AskToolTips from "./HocEx/AskToolTips";
import ErrorToolTips from "./HocEx/ErrorToolTips";
export default class HocExample extends Component {
constructor(props) {
super(props);
this.handlerAskConfirm = this.handlerAskConfirm.bind(this);
this.handlerAskCancel = this.handlerAskCancel.bind(this);
this.state = {
confirmTimes: 0,
cancelTimes: 0,
date: new Date().toLocaleString(),
msg: "我是一个错误提示框"
};
}
handlerAskConfirm(index) {
console.log("handlerAskConfirm:" + index);
this.setState((state, props) => {
const msg =
"睡在我下铺的兄弟Ask我:" +
(state.cancelTimes + index) +
"次!,Confirm:" +
index +
"次!Cancel:" +
state.cancelTimes +
"次!";
return {
confirmTimes: index,
msg: msg,
date: new Date().toLocaleString()
};
});
}
handlerAskCancel(index) {
console.log("handlerAskCancel:" + index);
this.setState((state, props) => {
const msg =
"睡在我下铺的兄弟Ask我:" +
(state.confirmTimes + index) +
"次!,Confirm:" +
state.confirmTimes +
"次!Cancel:" +
index +
"次!";
return {
cancelTimes: index,
msg: msg,
date: new Date().toLocaleString()
};
});
}
render() {
return (
<fieldset>
<legend>高阶组件实例</legend>
<ErrorToolTips
tipsType={{
type: "ErrorToolTips",
date: this.state.date
}}
msg={{ str: this.state.msg }}
/>
<AskToolTips
tipsType={{
type: "AskToolTips",
date: this.state.date
}}
confirmTimes={this.state.confirmTimes}
cancelTimes={this.state.cancelTimes}
onConfirm={e => this.handlerAskConfirm(e)}
onCancel={e => this.handlerAskCancel(e)}
/>
</fieldset>
);
}
}
我们直接调用AskToolTips和ErrorToolTips,并且传递props的方式和普通的组件没有上面不同。
效果:

高阶函数的具体应用 这里我就结合我们的 ReactDOM.createPortal 具体说一下:
Portal是一种将子节点渲染到存在于父组件以外的 DOM 节点的方案,相信大家都有所了解,这里也不做赘述,主要是通过 ReactDOM.createPortal 方法,第一个参数是任何可以渲染的react元素或者组件。第二个参数是作为渲染第一个参数的容器DOM 元素,最终生成的节点将会挂载到此dom元素节点上,是为了解决dom层级的问题
import React, { Component } from "react";
import ReactDOM from "react-dom";
const withPortal = WrappedComponent => {
class AddPortal extends Component {
constructor(props) {
super(props);
this.el = this.getDiv();
}
componentWillUnmount() {
document.body.removeChild(this.el);
}
getDiv() {
const div = document.createElement("div");
const appendNode = this.props.appendNode || document.body;
appendNode.appendChild(div);
return div;
}
render() {
return ReactDOM.createPortal(
<WrappedComponent {...this.props} />,
this.el
);
}
}
return AddPortal;
};
export default withPortal;
总结:高阶组件是一个函数,传递一个组件作为参数,并且会返回一个组件;在高阶组件中我们会对组件中公共的部分进行提取、抽象,可以对props,state进行控制,可以调用组件的生命周期方法,render方法,可以赋予组件一些具有共性的信息,但是我们不能对作为参数的组件进行修改,要保持他的纯函数的特性;通过对一系列组件的加工处理,可以大大提高我们的开发速度和销量,提高代码质量和组件易用性。
纯函数:如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数
tips:不要问我为什么不直接引用组件到高阶组件中,而不通过参数进行传递,如果这样的话,我们写每一个组件都要copy一份新的函数进行开发,何必呢