「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天前
|
前端开发 JavaScript 开发者
React 按钮组件 Button
本文介绍了 React 中按钮组件的基础概念,包括基本的 `&lt;button&gt;` 元素和自定义组件。详细探讨了事件处理、参数传递、状态管理、样式设置和可访问性优化等常见问题及其解决方案,并提供了代码示例。帮助开发者避免易错点,提升按钮组件的使用体验。
99 77
|
2天前
|
前端开发 UED 开发者
React 对话框组件 Dialog
本文详细介绍了如何在 React 中实现一个功能完备的对话框组件(Dialog),包括基本用法、常见问题及其解决方案,并通过代码案例进行说明。从安装依赖到创建组件、添加样式,再到解决关闭按钮失效、背景点击无效、键盘导航等问题,最后还介绍了如何添加动画效果和处理异步关闭操作。希望本文能帮助你在实际开发中更高效地使用 React 对话框组件。
96 75
|
7天前
|
前端开发 Java API
React 进度条组件 ProgressBar 详解
本文介绍了如何在 React 中创建进度条组件,从基础实现到常见问题及解决方案,包括动态更新、状态管理、性能优化、高级动画效果和响应式设计等方面,帮助开发者构建高效且用户体验良好的进度条。
35 18
|
21天前
|
存储 前端开发 测试技术
React组件的最佳实践
React组件的最佳实践
|
20天前
|
前端开发 API 开发者
React 文件上传组件 File Upload
本文详细介绍了如何在 React 中实现文件上传组件,从基础的文件选择和上传到服务器,再到解决文件大小、类型限制、并发上传等问题,以及实现多文件上传、断点续传和文件预览等高级功能,帮助开发者高效构建可靠的应用。
48 12
|
15天前
|
存储 前端开发 JavaScript
React 表单输入组件 Input:常见问题、易错点及解决方案
本文介绍了在 React 中使用表单输入组件 `Input` 的基础概念,包括受控组件与非受控组件的区别及其优势。通过具体代码案例,详细探讨了创建受控组件、处理多个输入字段、输入验证和格式化的方法,并指出了常见易错点及避免方法,旨在提升表单的健壮性和用户体验。
27 4
|
22天前
|
前端开发 JavaScript API
React 文件下载组件 File Download
本文介绍了在React中实现文件下载组件的方法,包括使用`a`标签和JavaScript动态生成文件,解决了文件路径、文件类型、大文件下载及文件名乱码等问题,并展示了使用第三方库`file-saver`和生成CSV文件的高级用法。
35 6
|
19天前
|
前端开发 JavaScript API
React 文件下载组件:File Download
本文详细介绍了如何在React应用中实现文件下载组件,包括基本概念、实现步骤和代码示例。同时,探讨了常见问题如文件类型不匹配、文件名乱码等及其解决方法,旨在提升用户体验和代码可维护性。
38 2
|
23天前
|
存储 前端开发 JavaScript
React 文件上传组件 File Upload
本文介绍了如何在 React 中实现文件上传组件,包括基本的概念、实现步骤、常见问题及解决方案。通过 `&lt;input type=&quot;file&quot;&gt;` 元素选择文件,使用 `fetch` 发送请求,处理文件类型和大小限制,以及多文件上传和进度条显示等高级功能,帮助开发者构建高效、可靠的文件上传组件。
65 2
|
23天前
|
存储 前端开发
在React框架中,如何使用对象来管理组件的状态
在React中,组件状态通过`state`对象管理,利用`setState`方法更新状态。状态变化触发组件重新渲染,实现UI动态更新。对象结构清晰,便于复杂状态管理。