「react进阶」一文吃透React高阶组件(HOC) (下)

简介: 从一篇文章彻底介绍 React 高阶组件

一 高阶组件源码级实践

hoc的应用场景有很多,也有很多好的开源项目,供我们学习和参考,接下来我真对三个方向上的功能用途,分别从源码角度解析HOC的用途。

1 强化prop- withRoute

用过withRoute的同学,都明白其用途,withRoute用途就是,对于没有被Route包裹的组件,给添加history对象等和路由相关的状态,方便我们在任意组件中,都能够获取路由状态,进行路由跳转,这个HOC目的很清楚,就是强化props,把Router相关的状态都混入到props中,我们看看具体怎么实现的。


function withRouter(Component) {
   
   
  const displayName = `withRouter(${
     
     Component.displayName || Component.name})`;
  const C = props => {
   
   
      /*  获取 */
    const {
   
    wrappedComponentRef, ...remainingProps } = props;
    return (
      <RouterContext.Consumer>
        {
   
   context => {
   
   
          return (
            <Component
              {
   
   ...remainingProps}
              {
   
   ...context}
              ref={
   
   wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.displayName = displayName;
  C.WrappedComponent = Component;
  /* 继承静态属性 */
  return hoistStatics(C, Component);
}

export default withRouter

withRoute的流程实际很简单,就是先从props分离出refprops,然后从存放整个route对象上下文RouterContext取出route对象,然后混入到原始组件的props中,最后用hoistStatics继承静态属性。至于hoistStatics我们稍后会讲到。

2 控制渲染案例 connect

由于connect源码比较长和难以理解,所以我们提取精髓,精简精简再精简, 总结的核心功能如下,connect的作用也有合并props,但是更重要的是接受state,来控制更新组件。下面这个代码中,为了方便大家理解,我都给简化了。希望大家能够理解hoc如何派发控制更新流的。

import store from './redux/store'
import {
   
    ReactReduxContext } from './Context'
import {
   
    useContext } from 'react'
function connect(mapStateToProps){
   
   
   /* 第一层: 接收订阅state函数 */
    return function wrapWithConnect (WrappedComponent){
   
   
        /* 第二层:接收原始组件 */
        function ConnectFunction(props){
   
   
            const [ , forceUpdate ] = useState(0)
            const {
   
    reactReduxForwardedRef ,...wrapperProps } = props

            /* 取出Context */
            const {
   
    store } = useContext(ReactReduxContext)

            /* 强化props:合并 store state 和 props  */
            const trueComponentProps = useMemo(()=>{
   
   
                  /* 只有props或者订阅的state变化,才返回合并后的props */
                 return selectorFactory(mapStateToProps(store.getState()),wrapperProps) 
            },[ store , wrapperProps ])

            /* 只有 trueComponentProps 改变时候,更新组件。  */
            const renderedWrappedComponent = useMemo(
              () => (
                <WrappedComponent
                  {
   
   ...trueComponentProps}
                  ref={
   
   reactReduxForwardedRef}
                />
              ),
              [reactReduxForwardedRef, WrappedComponent, trueComponentProps]
            )
            useEffect(()=>{
   
   
              /* 订阅更新 */
               const checkUpdate = () => forceUpdate(new Date().getTime())
               store.subscribe( checkUpdate )
            },[ store ])
            return renderedWrappedComponent
        }
        /* React.memo 包裹  */
        const Connect = React.memo(ConnectFunction)

        /* 处理hoc,获取ref问题 */  
        if(forwardRef){
   
   
          const forwarded = React.forwardRef(function forwardConnectRef( props,ref) {
   
   
            return <Connect {
   
   ...props} reactReduxForwardedRef={
   
   ref} reactReduxForwardedRef={
   
   ref} />
          })
          return hoistStatics(forwarded, WrappedComponent)
        } 
        /* 继承静态属性 */
        return hoistStatics(Connect,WrappedComponent)
    } 
}
export default Index

connect 涉及到的功能点还真不少呢,首先第一层接受订阅函数,第二层接收原始组件,然后用forwardRef处理ref,用hoistStatics 处理静态属性的继承,在包装组件内部,合并props,useMemo缓存原始组件,只有合并后的props发生变化,才更新组件,然后在useEffect内部通过store.subscribe()订阅更新。这里省略了Subscription概念,真正的connect中有一个Subscription专门负责订阅消息。

3 赋能组件-缓存生命周期 keepaliveLifeCycle

之前笔者写了一个react缓存页面的开源库react-keepalive-router,可以实现vuekeepalive + router功能,最初的版本没有缓存周期的,但是后来热心读者,期望在被缓存的路由组件中加入缓存周期,类似activated这种的,后来经过我的分析打算用HOC来实现此功能。

于是乎react-keepalive-router加入了全新的页面组件生命周期 activedunActived, actived 作为缓存路由组件激活时候用,初始化的时候会默认执行一次 , unActived 作为路由组件缓存完成后调用。但是生命周期需要用一个 HOC 组件keepaliveLifeCycle 包裹。

使用

import React   from 'react'
import {
   
    keepaliveLifeCycle } from 'react-keepalive-router'

@keepaliveLifeCycle
class index extends React.Component<any,any>{
   
   

    state={
   
   
        activedNumber:0,
        unActivedNumber:0
    }
    actived(){
   
   
        this.setState({
   
   
            activedNumber:this.state.activedNumber + 1
        })
    }
    unActived(){
   
   
        this.setState({
   
   
            unActivedNumber:this.state.unActivedNumber + 1
        })
    }
    render(){
   
   
        const {
   
    activedNumber , unActivedNumber } = this.state
        return <div  style={
   
   {
   
    marginTop :'50px' }}  >
           <div> 页面 actived 次数: {
   
   activedNumber} </div>
           <div> 页面 unActived 次数:{
   
   unActivedNumber} </div>
        </div>
    }
}
export default index

效果:

lifecycle.gif

原理

import {
   
   lifeCycles} from '../core/keeper'
import hoistNonReactStatic from 'hoist-non-react-statics'
function keepaliveLifeCycle(Component) {
   
   
   class Hoc extends React.Component {
   
   
    cur = null
    handerLifeCycle = type => {
   
   
      if (!this.cur) return
      const lifeCycleFunc = this.cur[type]
      isFuntion(lifeCycleFunc) && lifeCycleFunc.call(this.cur)
    }
    componentDidMount() {
   
    
      const {
   
   cacheId} = this.props
      cacheId && (lifeCycles[cacheId] = this.handerLifeCycle)
    }
    componentWillUnmount() {
   
   
      const {
   
   cacheId} = this.props
      delete lifeCycles[cacheId]
    }
     render=() => <Component {
   
   ...this.props} ref={
   
   cur => (this.cur = cur)}/>
  }
  return hoistNonReactStatic(Hoc,Component)
}

keepaliveLifeCycle 的原理很简单,就是通过ref或获取 class 组件的实例,在 hoc 初始化时候进行生命周期的绑定, 在 hoc 销毁阶段,对生命周期进行解绑, 然后交给keeper统一调度,keeper通过调用实例下面的生命周期函数,来实现缓存生命周期功能的。

二 高阶组件的注意事项

1 谨慎修改原型链

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

这样做会产生一些不良后果。比如如果你再用另一个同样会修改 componentDidMountHOC 增强它,那么前面的 HOC 就会失效!同时,这个 HOC 也无法应用于没有生命周期的函数组件。

2 继承静态属性

在用属性代理的方式编写HOC的时候,要注意的是就是,静态属性丢失的问题,前面提到了,如果不做处理,静态方法就会全部丢失。

手动继承

我们可以手动将原始组件的静态方法copyhoc组件上来,但前提是必须准确知道应该拷贝哪些方法。

function HOC(Component) {
   
   
  class WrappedComponent extends React.Component {
   
   
      /*...*/
  }
  // 必须准确知道应该拷贝哪些方法 
  WrappedComponent.staticMethod = Component.staticMethod
  return WrappedComponent
}

引入第三方库

这样每个静态方法都绑定会很累,尤其对于开源的hoc对原生组件的静态方法是未知的,我们可以使用 hoist-non-react-statics 自动拷贝所有的静态方法:

import hoistNonReactStatic from 'hoist-non-react-statics'
function HOC(Component) {
   
   
  class WrappedComponent extends React.Component {
   
   
      /*...*/
  }
  hoistNonReactStatic(WrappedComponent,Component)
  return WrappedComponent
}

3 跨层级捕获ref

高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。我们可以通过forwardRef来解决这个问题。

/**
 * 
 * @param {*} Component 原始组件
 * @param {*} isRef  是否开启ref模式
 */
function HOC(Component,isRef){
   
   
  class Wrap extends React.Component{
   
   
     render(){
   
   
        const {
   
    forwardedRef ,...otherprops  } = this.props
        return <Component ref={
   
   forwardedRef}  {
   
   ...otherprops}  />
     }
  }
    if(isRef){
   
   
      return  React.forwardRef((props,ref)=> <Wrap forwardedRef={
   
   ref} {
   
   ...props} /> )
    }
    return Wrap
}

class Index extends React.Component{
   
   
  componentDidMount(){
   
   
      console.log(666)
  }
  render(){
   
   
    return <div>hello,world</div>
  }
}

const HocIndex =  HOC(Index,true)

export default ()=>{
   
   
  const node = useRef(null)
  useEffect(()=>{
   
   
     /* 就可以跨层级,捕获到 Index 组件的实例了 */ 
    console.log(node.current.componentDidMount)
  },[])
  return <div><HocIndex ref={
   
   node}  /></div>
}

打印结果:

forwardRef.jpg

如上就解决了,HOC跨层级捕获ref的问题。

4 render中不要声明HOC

🙅错误写法:

class Index extends React.Component{
   
   
  render(){
   
   
     const WrapHome = HOC(Home)
     return <WrapHome />
  }
}

如果这么写,会造成一个极大的问题,因为每一次HOC都会返回一个新的WrapHome,react diff会判定两次不是同一个组件,那么每次Index 组件 render触发,WrapHome,会重新挂载,状态会全都丢失。如果想要动态绑定HOC,请参考如下方式。

🙆正确写法:

const WrapHome = HOC(Home)
class index extends React.Component{
   
   
  render(){
   
   
     return <WrapHome />
  }
}

三 总结

本文从高阶组件功能为切入点,介绍二种不同的高阶组件如何编写,应用场景,以及实践。涵盖了大部分耳熟能详的开源高阶组件的应用场景,如果你觉得这篇文章对你有启发,最好还是按照文章中的demo,跟着敲一遍,加深印象,知道什么场景用高阶组件,怎么用高阶组件。

实践是检验真理的唯一标准,希望大家能把高阶组件起来,用起来。

最后 , 送人玫瑰,手留余香,觉得有收获的朋友可以给笔者点赞,关注一波 ,陆续更新前端超硬核文章。

回顾往期react经典好文

react进阶系列

react源码系列

react-hooks系列

开源项目系列

参考文献

react中文文档

相关文章
|
1月前
|
前端开发 API
react怎么实现父子组件传值?
react怎么实现父子组件传值?
10 1
|
12天前
|
缓存 前端开发 JavaScript
【亮剑】在React中如何通过点击事件控制组件显示与隐藏,包括基础概念和高级应用
【4月更文挑战第30天】本文介绍了在React中如何通过点击事件控制组件显示与隐藏,包括基础概念和高级应用。使用`useState`钩子和Context API可实现状态驱动的条件渲染,通过CSS类控制组件样式,或利用React Portals在DOM不同位置渲染。性能优化应注意避免不必要的渲染、合理使用Keys、优化事件处理器、使用Memoization及清理资源。测试和验证确保逻辑正确性,以构建动态用户界面并提升应用性能。
|
17天前
|
前端开发 JavaScript 开发者
vue3、react组件数据传值对比分析——父组件传递子组件,子组件传递父组件(二)
vue3、react组件数据传值对比分析——父组件传递子组件,子组件传递父组件
22 0
|
17天前
|
JavaScript 前端开发 算法
vue3、react组件数据传值对比分析——父组件传递子组件,子组件传递父组件(一)
vue3、react组件数据传值对比分析——父组件传递子组件,子组件传递父组件
17 0
|
17天前
|
存储 前端开发 JavaScript
React的表单处理:受控组件与非受控组件深入解析
【4月更文挑战第25天】React表单处理涉及受控和非受控组件。受控组件通过状态管理表单数据,每次用户输入都触发状态更新,确保数据同步,适合实时交互但可能影响性能。非受控组件不直接管理状态,数据存储在DOM中,简化代码,适用于更新不频繁的场景,但在数据验证和同步上存在挑战。开发者应根据需求灵活选择。
|
17天前
|
前端开发 JavaScript 开发者
React的函数组件与类组件:探索两者之间的区别
【4月更文挑战第25天】React提供函数组件和类组件,两者在语法、状态管理、生命周期和性能优化上有所不同。函数组件简单且易于理解,使用 Hooks 可添加状态管理;类组件支持复杂状态管理和生命周期方法,适用于需要精细控制更新的场景。随着 Hooks 的发展,函数组件功能增强,成为更多开发者的首选。选择组件类型应根据实际需求权衡。
|
17天前
|
设计模式 前端开发 API
React的高阶组件(HOC):使用与设计模式探讨
【4月更文挑战第25天】React的高阶组件(HOC)是一种复用和增强组件的高级模式,它接受组件并返回新组件。非侵入式增强使得HOC能在不修改原有组件代码的情况下添加功能。定义HOC后,将其应用于目标组件并渲染增强后的组件。常见设计模式包括属性代理、控制反转和装饰器。然而,使用时要注意避免滥用,保持命名清晰,关注性能优化。理解并恰当使用HOC能提升React应用的构建效率。
|
18天前
|
前端开发
探索React Hooks:一种全新的组件逻辑管理方式
React Hooks是React 16.8版本引入的一项新功能,它改变了我们编写React组件的方式。本文将从Hooks的起源讲起,逐步分析Hooks的优势,并通过具体示例展示Hooks在组件逻辑管理中的应用,旨在帮助读者更好地理解和运用React Hooks。
|
1月前
|
前端开发
React组件通信:如何优雅地实现组件间的数据传递
React组件通信:如何优雅地实现组件间的数据传递
21 0
|
3月前
|
前端开发 中间件 数据安全/隐私保护
React路由进阶方法
React路由进阶方法
28 1