React学习 --- 高阶组件

662 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

在JS中只要满足以下两个条件中的一个,就可以被称之为高阶函数:

  1. 接受一个或多个函数作为输入
  2. 输出一个函数

JavaScript中比较常见的filter、map、reduce都是高阶函数

而React中同样存在高阶组件的概念:

高阶组件的英文是 Higher-Order Components,简称为 HOC

官方的定义: 高阶组件是参数为组件,返回值为新组件的函数

  • 高阶组件 本身不是一个组件,而是一个函数
  • 这个函数的参数是一个组件,返回值也是一个组件;

高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

const EnhancedComponent = higherOrderComponent(WrappedComponent)

一个简单的HOC

import { PureComponent } from 'react'

class App extends PureComponent {
  render() {
    return (
      <div>
        App: { this.props.name }
      </div>
    )
  }
}

// 参数是一个组件,所以参数名推荐首字母大写
function HOC(Wrapped) {
  // 在React DevTool中组件的名称默认是组件的class后的名称
  // 如果是类表达式,那么组件的名称就是类父组件的名称
  const NewCpn = class extends PureComponent {
    render() {
      // HOC只是一个包裹的组件,实际使用props的Wrapped组件
      // 所以需要进行参数的传递
      return <Wrapped {...this.props}/>
    }
  }

  // 每一个类组件都有一个displayName
  // 可以显示的设置类组件在React DevTool中显示的名称
  NewCpn.displayName = 'HOC'

  return NewCpn
}

export default HOC(App)
< App name="App" />
// HOC要求返回值和参数都是一个组件
// 所以HOC返回值和参数接收的组件即可以是类组件,也可以是函数组件
function HOC(Wrapped) {
  const NewCpn = function(props) {
    return <Wrapped {...props} />
  }

  NewCpn.displayName = 'HOC'
  return NewCpn
}

案例

劫持props

使用props共享相同的属性

Cpn

import { PureComponent } from 'react'

class Cpn extends PureComponent {
  render() {
    return (
      <ul>
        <li>nickname: {this.props?.nickname}</li>
        <li>level: {this.props?.level}</li>
      </ul>
    )
  }
}

function enhanceLevel(Wrapped) {
 return function(props) {
   // 在原本组件基础上扩充level属性
   return <Wrapped {...props} level={66} />
 }
}

export default enhanceLevel(Cpn)

App

import { PureComponent } from 'react'

import Cpn from './Cpn'

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Cpn nickname="coderwxf"/>
      </div>
    )
  }
}

使用context共享相同的属性

Cpn

import { PureComponent, createContext } from 'react'

export const userContext = createContext({
  nickname: 'anonyamous',
  level: -1
})

class Cpn extends PureComponent {
  render() {
    return (
      <ul>
        <li>nickname: {this.props?.nickname}</li>
        <li>level: {this.props?.level}</li>
      </ul>
    )
  }
}

// 参考withRouter,HOC方法一般使用with开头 + 大驼峰命名
function withUser(Wrapped) {
 return function(props) {
   return (
    <userContext.Consumer>
      {
        user => {
          return <Wrapped {...props} {...user} />
        }
      }
    </userContext.Consumer>
   )
 }
}

export default withUser(Cpn)

App

import { PureComponent } from 'react'

import Cpn, {userContext} from './Cpn'

export default class App extends PureComponent {
  render() {
    return (
      <div>
      	<userContext.Provider value={{nickname: 'Klaus', level: 6}}>
          <Cpn />
        </userContext.Provider>
        
        {/* 使用context的默认值 */}
        <Cpn />
      </div>
    )
  }
}

劫持JSX

Cpn

import { PureComponent } from 'react'

class Login extends PureComponent {
  render() {
    return <h2>Login</h2>
  }
}


// define HOC
function withAuth(WrappedComponent) {
  // 这里之所以定义一个具名函数,是为了在ReactDevTool工具中可以很方便的查找该组件
  // 即给该组件一个具体的名称
  return function Auth(props) {
    const { isLogin } = props
    return isLogin ? <WrappedComponent /> : <Login />
  }
}

class Cpn extends PureComponent {
  render() {
    return (
      <div>
        Cpn
      </div>
    )
  }
}

export default withAuth(Cpn)

App

import { PureComponent } from 'react'
import Cpn from './Cpn'

export default class App extends PureComponent {
  render() {
    return (
      <Cpn isLogin={false} />
    )
  }
}

劫持生命周期

Cpn.js

import { PureComponent } from 'react'

class Cpn extends PureComponent {
  render() {
    return (
      <div>
        Cpn
      </div>
    )
  }
}

function withRenderTime(Wrapped) {
  return class HOC extends PureComponent {
    render() {
      this.startTime = Date.now()
      return <Wrapped {...this.props} />
    }

    componentDidMount() {
      // 无论类组件还是函数组件,本质上都是构造函数
      // 所以组件上有一个name属性,表示当前组件的名称
      console.log(`${Wrapped.name}渲染时间为${Date.now() - this.startTime}ms`)
    }

  }
}

export default withRenderTime(Cpn)

App

import { PureComponent } from 'react'

import Cpn from './Cpn'

export default class App extends PureComponent {
  render() {
    return (
      <Cpn />
    )
  }
}

HOC的意义

HOC最大的作用就是可以对传入的组件进行劫持,也就是可以将多个组件所共同的逻辑抽离到HOC中进行实现 或者 可以通过HOC对组件进行功能扩展

早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的Mixin中的方法可能会相互冲突
  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

当然,HOC也有自己的一些缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;

Cpn

import { PureComponent, createContext } from 'react'

export const userContext = createContext({
  nickname: 'anonyamous',
  level: -1
})

class Cpn extends PureComponent {
  render() {
    return (
      <ul>
        <li>nickname: {this.props?.nickname}</li>
        <li>level: {this.props?.level}</li>
      </ul>
    )
  }
}

function withUser(Wrapped) {
 return function(props) {
   return (
    <userContext.Consumer>
      {
        user => {
          // 此时如果user和props中有prop的名称是同名的时候
          // 就会发生冲突,后一个会将前一个给覆盖
          return <Wrapped {...user} {...props} />
        }
      }
    </userContext.Consumer>
   )
 }
}

export default withUser(Cpn)

App

import { PureComponent } from 'react'

import Cpn, {userContext} from './Cpn'

export default class App extends PureComponent {
  render() {
    return (
      <div>
      	<userContext.Provider value={{nickname: 'Klaus', level: 6}}>
          <Cpn nickname="coderwxf"/>
        </userContext.Provider>
      </div>
    )
  }
}

Ref转发

ref不能应用于函数式组件,因为函数式组件没有实例,所以不能获取到对应的组件对象

为此React为我们提供了一个HOC,即为forwardRef, 可以帮助我们获取React的函数组件

import { PureComponent, forwardRef, createRef } from 'react'

// 使用forwardRef这个HOC进行包裹后,函数组件会多传入一个参数,即为ref
// 也就是在实际使用HOC返回的组件时候传入的ref
// 我们可以通过这个ref参数来获取函数组件中对应元素所对应的DOM对象
const FunCpn = forwardRef(function(props, ref) {
  return <p ref={ref}>funCpn</p>
})

export default class Cpn extends PureComponent {
  constructor(props) {
    super(props)

    this.funCpnRef = createRef()
  }

  render() {
    return (
      <div>
        {/*
          默认情况下,ref属性是不可以作为组件的props进行传递的
          因为ref属性是一个特殊的属性,会交给react内部进行管理
        */}
        <FunCpn ref={ this.funCpnRef }/>
        <button onClick={() => this.handleClick()}>获取函数组件对应的DOM对象</button>
      </div>
    )
  }

  handleClick() {
    console.log(this.funCpnRef.current)
  }
}