高阶组件是对既有组件进行包装,以增强既有组件的功能。其核心实现是一个无状态组件(函数),接收另一个组件作为参数,然后返回一个新的功能增强的组件。
看一个最简单的例子:
App.js:
import React,{ PureComponent } from "react";
const DisplayUser = (props) => {
return(
<div>
姓名:{ props.username }
</div>
);
}
const HOC = (InnerComponent) => {
return class HOCComponent extends PureComponent{
render(){
const { username } = this.props;
return(
<InnerComponent username = { username } />
);
}
}
}
export default HOC(DisplayUser);
index.js:
import React from "react";
import ReactDOM from "react-dom";
import HOC from "./App";
ReactDOM.render(<HOC username = "Mike" birth = "1999-09-09" />,document.getElementById("root"));
HOC 函数接受一个组件作为参数,返回一个新的组件,新组件中渲染了参数组件,这样就可以对参数组件做一些配置,例如本例的过滤 props,只传递给参数组件合法的 props,不传递非法的 props。
高阶组件的分类
高阶组件分为两种类型:
- 代理型高阶组件
- 继承型高阶组件
上面的例子中的高阶组件类型就是代理型高阶组件。
二者的主要区别是对参数组件的操作上,代理型高阶组件是返回一个新的组件类,该类继承于根组件(Component 或 PureComponent),然后让这个新的组件类去渲染参数组件,这样就可以对参数组件实现包装或代理 props 的操作。继承型高阶组件也是返回一个新的组件类,但这个新的类不继承根组件类,而是继承于参数组件类,这样我们就可以重写参数组件类中的一些方法。
这里的高阶组件应该叫做高阶组件生成函数,其执行的返回值才叫高阶组件,为了方便我们这里统一称为高阶组件了,请知悉。
下面分别来说下这两类的高阶组件。
代理型高阶组件
代理型高阶组件接受一个组件作为参数,返回一个新的继承于根组件(Component、PureComponent)的组件,并用该新组件渲染参数组件,其基本结构为:
export const HOC = (SomeComponent) => {
return class HOCComponent extends PureComponent{
render(){
doSomeThing()...
return(
<SomeComponent />
);
}
}
}
代理型高阶组件有以下几个作用:
- 代理 props
- 访问 ref
- 抽取状态
- 包装组件
代理 props
代理型高阶组件可以接收被包装组件的 props,并对这些 props 进行操作,如增删改操作。在渲染被包装组件时,传入被修改的 props,如本文开头的例子。
基本结构如下:
export const HOC = (SomeComponent) => {
return class HOCComponent extends PureComponent{
render(){
const newProps = doSomeThing(this.props);
return(
<SomeComponent {...newProps} />
);
}
}
}
访问 ref
ref 是一个特殊的属性,可以声明为函数,如果 ref 属性是一个函数,那么将在组件装载完成后自动执行该函数,并将当前组件的实例传入该函数中。
看一个栗子:
class SubComponent extends PureComponent{
render(){
return(
<div className = "box">
我想要被获取!!!
</div>
);
}
}
const HOC = (InnerComponent) => {
return class HOCComponent extends PureComponent{
constructor(...args){
super(...args);
this.handSubRef = this.handSubRef.bind(this);
}
// 获取子组件的实例
// 该函数在子组件被装载后自动调用
handSubRef(subEle){
console.log(subEle);
}
render(){
return(
<InnerComponent ref = { this.handSubRef } />
);
}
}
}
export default HOC(SubComponent);
这样,在参数组件被装载后,就会自动执行 handSubRef 函数,并将参数组件的实例传入 handSubRef 函数。在 handSubRef 中可以获取参数组件的一些列属性,如 state、props 等。
注意,如果参数组件是一个无状态组件,那么传入 handSubRef 的参数将是 null。
const SubComponent = (props) => {
return(
<div className = "box">
我想要被获取!!!
</div>
);
}
const HOC = (InnerComponent) => {
return class HOCComponent extends PureComponent{
constructor(...args){
super(...args);
this.handSubRef = this.handSubRef.bind(this);
}
// 参数组件是无状态组件
// subEle 为 null
handSubRef(subEle){
console.log(subEle);
}
render(){
return(
<InnerComponent ref = { this.handSubRef } />
);
}
}
}
export default HOC(SubComponent);
抽取状态
当一个组件的功能较复杂时,我们建议将组件拆分为容器组件和UI组件,UI组件负责展示,容器组件用来进行逻辑管理。这个步骤就是“抽取状态”。
我们将前面的计算器应用中的组件进行一些修改:
...
const UICounter = (props) => {
const { increase,decrease,value} = props;
return(
<div className = "counter">
<div>
<button onClick = { increase }>+</button>
<button onClick = { decrease }>-</button>
</div>
<span>当前的值为:{ value }</span>
</div>
);
}
const HOC = (SubEle) => {
return class HOCComponent extends PureComponent{
constructor(props) {
super(props);
// 获取初始状态
this.state = {
value:store.getState().value,
};
}
componentWillMount() {
// 监听 store 变化
store.subscribe(this.watchStore.bind(this));
}
componentWillUnmount() {
// 对 store 变化取消监听
store.unsubscribe(this.watchStore.bind(this));
}
// 监听回调函数,当 store 变化后执行
watchStore(){
// 回调函数中重新设置状态
this.setState(this.getCurrentState());
}
// 从 store 中获取状态
getCurrentState(){
return{
value:store.getState().value,
}
}
// 增加函数
increase(){
// 派发 INCREMENT Action
store.dispatch(ACTIONS.increament());
}
// 减少函数
decrease(){
// 派发 DECREAMENT Action
store.dispatch(ACTIONS.decreament());
}
render(){
return(
<UICounter
increase = { this.increase.bind(this)}
decrease = { this.decrease.bind(this)}
value = { this.state.value }
/>
);
}
}
}
export default HOC(UICounter);
...
包装组件
包装组件就是对某些组件进行组合,返回不同的组合形式,同时可以设置展示样式。如将展示性 select 和输入表单包装成可搜索的 select等。
基本结构如下:
// 可以传入一个数组,或者单个组件
const HOC = (SubComponents) => {
return class HOCComponent extends PureComponent{
render(){
const style = ...
// 将子组件组合成一个大组件,同时可以修改样式
return(
<div style = { style }>
{...SubComponents}
</div>
);
}
}
}
export default HOC([Select,SearchBox]);
至此,代理型高阶组件就说完了,下面说说继承型高阶组件。
继承型高阶组件
和代理型高阶组件创建一个继承于根组件(Component、PureComponent)的组件类不同的是,继承型高阶组件是创建一个继承于参数组件的组件类,以此可以覆写父组件中的一些方法。
最常用的应用是重写父组件的生命周期函数:
const HOC = (ParComponents) => {
return class HOCComponent extends ParComponents{
// 覆写父组件的生命周期函数
shouldComponentUpdate(nextProps, nextState) {
...
}
render(){
// 渲染时仍然按照父组件的 render 函数进行渲染
return super.render();
}
}
}
export default HOC(ParComponents);
子组件覆写了父组件的生命周期方法,以获取更好的渲染性能,在渲染(render)时,还是调用父类的 render 方法。
函数作为子组件
前面用高阶组件实现了代理 props 功能,对于特定的组件,需要传入特定的 props 名,由于每个组件需要的 props 可能有差异,如果使用高阶组件对参数组件进行判断,再传入合适的 props 名显然太麻烦了。这时如果我们需要更通用的方案,可以接收函数作为子组件,利用函数的形参的特性来排除组件 props 名之间的差异性。看一下演示代码:
...
// 此组件需要使用 user props
const SubComponent1 = (user) => {
return(
<TestComponent user = { user } />
);
}
// 此组件需要使用 username props
const SubComponent2 = (user) => {
return(
<TestComponent2 username = { user } />
);
}
// 高阶组件接受 testUser 作为 props
const HOC = (props) => {
const { testUser } = props;
return(
props.children(testUser)
);
}
...
使用此高阶组件传递 props:
...
<HOC testUser = "MIKE">
{ SubComponent1 }
</HOC>
<HOC testUser = "JACK">
{ SubComponent2 }
</HOC>
...
高阶组件接收一个函数作为子组件,然后调用这个组件函数,并传入相应的 props,函数调用的结果返回一个组件,组件需要接收什么样的 props 名可以自行定义,不用在高阶组件中进行判断了。
以函数作为组件的形式主要用于不同的组件需要接收同一份 props,但是它们需要的 props 名称不一样的情况,可以用函数的形参巧妙的化解掉父组件的判断。虽然用途较少,但不失为一个优秀的模式,将 children 进行调用简直是神来之笔有木有。
总结
本文谈到了 React 中的高阶组件,高阶组件主要包括两种类型:
- 代理型高阶组件
- 继承型高阶组件
代理型高阶组件的主要用途是:
- 代理子组件的 props(对 props 进行增删改操作)
- 获取子组件的 ref(实例)
- 抽取子组件状态(拆分逻辑)
- 将子组件进行包装(组合子组件,调整样式等)
继承型高阶组件的主要用途是覆写父组件的生命周期方法,通常是 shouldComponentUpdate 方法,以此来提高渲染性能,渲染时调用父类的 render 函数(super.render())。
最后,介绍了函数作为子组件的情形,主要用于不同的组件接受同一份 props,但是它们各自需要的 props 名不同的情况,利用函数的形参巧妙化解父组件的额外判断操作。这是一种优秀的模式,让人耳目一新。
完。