跳至主要內容

高阶组件(HOC)

Mr.LRH大约 7 分钟

高阶组件(HOC)

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数

常见高阶组件

属性代理

function proxyPropsHOC(Component) {
  return class extends React.Component {
    render() {
      // 过滤掉非此 HOC 额外的 props,且不要进行透传
      const { extraProp, ...passThroughProps } = this.props

      // 将 props 注入到被包装的组件中,
      // 通常为 state 的值或者实例方法。
      const injectedProp = someStateOrInstanceMethod

      // 将 props 传递给被包装组件
      return <Component injectedProp={injectedProp} {...passThroughProps} />
    }
  }
}

属性代理,将原始组件包装在容器组件中,组成新的组件,可以对原始组件进行一些强化操作。

  • 优点:
    • 属性代理可以和业务组件低耦合,零耦合。对于条件渲染和 props 属性增强,只需负责控制子组件渲染和传递额外的 props ,无须知道业务组件的处理逻辑。
    • 适用于类组件和函数组件。
    • 可以完全隔离业务组件的渲染,因为 HOC 返回的是一个新的组件,可以完全控制业务组件是否渲染。
    • 可以嵌套使用,多个 HOC 是可以嵌套使用的,而且一般不会限制包装 HOC 的先后顺序。
  • 缺点:
    • 一般无法直接获取原始组件的状态,如果想要获取,需要 ref 获取组件实例。
    • 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
    • 因为本质上是产生了一个新组件,所以需要配合 forwardRef 来转发 ref

反向继承

class HelloWorld extends React.Component {
  render() {
    return <div> Hello World!</div>
  }
}

function reverseExtendHOC(Component) {
  return class extends Component {
    /* 直接继承需要包装的组件 */
  }
}

const ExampleComponent = reverseExtendHOC(HelloWorld)

反向继承,在于包装后的组件继承了原始组件本身,无须再挂载业务组件。

  • 优点:
    • 方便获取组件内部状态,比如: stateprops ,生命周期,绑定的事件函数等。
    • ES6 继承可以良好继承静态属性。所以无须对静态属性和方法进行额外的处理。
  • 缺点:
    • 函数组件无法使用。
    • 和被包装的组件耦合度高,需要知道被包装的原始组件的内部状态,具体进行的处理逻辑。
    • 如果多个反向继承 HOC 嵌套在一起,当前状态会覆盖上一个状态。比如,有多个 componentDidMount ,当前 componentDidMount 会覆盖上一个 componentDidMount 。这样副作用串联起来,影响很大。

高阶组件功能

强化 Props

以 React Router 提供的 withRouter 的 HOC 为例。将路由对象和原始 props 传递给原始组件,则可以在原始组件中获取 history ,location 等信息。

function withRouter(Component) {
  const displayName = `withRouter(${Component.displayName || Component.name})`

  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props
    return (
      // 用 Context.Consumer 上下文模式获取保存的路由信息。( React Router 中路由状态是通过 context 上下文保存传递的)
      <RouterContext.Consumer>
        {context => {
          return (
            <Component
              {...remainingProps} // 组件原始的 props
              {...context} // 存在路由对象的上下文,history,location 等
              ref={wrappedComponentRef} // 用于转发 ref
            />
          )
        }}
      </RouterContext.Consumer>
    )
  }

  C.displayName = displayName
  C.WrappedComponent = Component

  /* 继承静态属性 */
  return hoistStatics(C, Component)
}

控制渲染

渲染劫持

const renderHijackHOC = WrapComponent =>
  class extends WrapComponent {
    render() {
      if (this.props.visible) {
        return super.render()
      } else {
        return <div>暂无数据</div>
      }
    }
  }

修改渲染树

class FrameworkList extends React.Component {
  render() {
    return (
      <div>
        <ul>
          <li> React </li>
          <li> Vue </li>
          <li> Angular </li>
        </ul>
      </div>
    )
  }
}

function changeRenderTreeHOC(Component) {
  return class Advance extends Component {
    render() {
      const element = super.render()
      const otherProps = {
        name: 'other props name',
      }

      const appendElement = React.createElement(
        'li',
        {},
        `this is ${otherProps.name}`
      )
      const newChild = React.Children.map(
        element.props.children.props.children,
        (child, index) => {
          if (index === 2) return appendElement
          return child
        }
      )
      return React.cloneElement(element, element.props, newChild)
    }
  }
}

动态加载

function dynamicLoadingHOC(loadRouter) {
  return class Content extends React.Component {
    state = { Component: null }

    componentDidMount() {
      if (this.state.Component) return
      loadRouter()
        .then(module => module.default) // 动态加载 component 组件
        .then(Component => this.setState({ Component }))
    }

    render() {
      const { Component } = this.state
      return Component ? <Component {...this.props} /> : <Loading />
    }
  }
}

组件赋能

Ref 获取实例

通过 ref 获取组件实例,可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件。需要注意的是类组件才存在实例,函数组件不存在实例。

function getInstanceHOC(Component) {
  return class extends React.Component {
    constructor() {
      super()
      this.node = null /* 获取组件实例,可以进行相关操作 */
    }

    render() {
      return <Component {...this.props} ref={node => (this.node = node)} />
    }
  }
}

事件监控

function ClickMonitorHOC(Component) {
  return function Wrap(props) {
    const dom = useRef(null)

    useEffect(() => {
      const handerClick = () => console.log('发生点击事件')
      dom.current.addEventListener('click', handerClick)

      return () => dom.current.removeEventListener('click', handerClick)
    }, [])

    return (
      <div ref={dom}>
        <Component {...props} />
      </div>
    )
  }
}

@ClickMonitorHOC // 装饰器模式
class HelloWorld extends React.Component {
  render() {
    return (
      <div className="index">
        <p>Hello World!</p>
        <button>按钮</button>
      </div>
    )
  }
}

注意事项

不要改变原始组件,而应该使用组合

不要在 HOC 中修改组件原型(或以其他方式改变它)。

  • 输入组件无法像 HOC 增强那样进行使用
  • 如果存在另外一个同样修改原型(例如,修改 componentDidMount 生命周期)的 HOC 进行增强,该 HOC 修改原型的功能就会失效。
// 错误用法 !!!
function exampleHOC(Component) {
  const prototypeDidMount = Component.prototype.componentDidMount

  Component.prototype.componentDidMount = function () {
    console.log('劫持生命周期: componentDidMount')
    prototypeDidMount.call(this)
  }

  return Component
}

HOC 不应该修改传入组件,而应该使用组合的方式,通过将组件包装在容器组件中实现功能。

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('Current props: ', this.props)
      console.log('Previous props: ', prevProps)
    }

    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

不要在 render 方法中使用 HOC

React 的 diff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

在 render 方法中使用 HOC,将导致子树每次渲染都会进行卸载,和重新挂载的操作。不仅仅会影响性能,同时会导致该组件及其所有子组件的状态丢失。

在极少数情况下,需要动态调用 HOC。可以在组件的生命周期方法或其构造函数中进行调用。

// 错误用法 !!!
class ExampleComponent extends React.Component {
  render() {
    // 每次调用 render 函数都会创建一个新的 EnhancedComponent
    const EnhancedComponent = HOC(Home)
    // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
    return <EnhancedComponent />
  }
}

// 错误用法 !!!
function ExampleComponent() {
  const EnhancedComponent = HOC(Home)
  return <EnhancedComponent />
}

Refs 的处理

高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。

因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。

这个问题的解决方案是通过使用 React.forwardRef API 进行 ref 转发处理。

多个 HOC 嵌套顺序

多个 HOC 嵌套,需要注意 HOC 的顺序,分析出各个 HOC 之间是否有依赖关系。

  • 如果两个 HOC 相互之间有依赖,比如 HOC1 依赖于 HOC2 ,那么 HOC1 应该在 HOC2 内部。
  • 如果需要通过 HOC 方式给原始组件添加一些额外生命周期,因为涉及到获取原始组件的实例 instance ,那么当前的 HOC 应该离原始组件最近。
// 类组件 :可以用装饰器模式,对类组件进行包装
@HOC1(styles)
@HOC2
@HOC3
class ExampleComponent extends React.Component {
  render() {
    return <div>ExampleComponent</div>
  }
}

// 函数组件
function ExampleComponent() {
  return <div>ExampleComponent</div>
}
const EnhancedComponent = HOC1(styles)(HOC2(HOC3(ExampleComponent)))

务必复制静态方法

HOC 本质上是返回了一个新的 component ,如果原来的 component 绑定一些静态属性方法,如果不处理,新的 component 上就会丢失这些静态属性。

  • 手动继承

    function enhance(WrappedComponent) {
      class Enhance extends React.Component {
        /*...*/
      }
    
      // 必须准确知道应该拷贝哪些方法 :(
      Enhance.staticMethod = WrappedComponent.staticMethod
    
      return Enhance
    }
    
  • 使用第三方库

    import hoistNonReactStatic from 'hoist-non-react-statics'
    
    function enhance(WrappedComponent) {
      class Enhance extends React.Component {
        /*...*/
      }
    
      // 使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法
      hoistNonReactStatic(Enhance, WrappedComponent)
    
      return Enhance
    }
    

示例

权限校验 HOC

  • 使用两层包装函数的 HOC,第一层用于获取 HOC 绑定的当前组件的权限签名,因为需要使用该权限签名和权限列表进行匹配。第二层接受的原始组件。
  • 在 HOC 中用 Context.Consumer 接收权限列表,做权限匹配。组件有权限展示,没有权限展示无权限组件。
function NoPermission() {
  return <div>您暂时没有权限,请联系管理员开通权限!</div>
}

/* 权限检验 HOC */
export function PermissionHOC(authorization) {
  return function (Component) {
    return function (props) {
      /* 匹配权限 */
      const matchPermission = (value, list) => list.indexOf(value)

      return (
        <Permission.Consumer>
          {permissionList =>
            matchPermission(authorization, permissionList) >= 0 ? (
              <Component {...props} />
            ) : (
              <NoPermission />
            )
          }
        </Permission.Consumer>
      )
    }
  }
}

// 使用

// 绑定文档录入页面
@PermissionHoc('writeDoc')
export default class ExampleComponent extends React.Component {}

//绑定标签录入页面
export default PermissionHoc('writeTag')(index)
上次编辑于:
贡献者: lingronghai