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

一 前言

React高阶组件(HOC),对于不少react开发者来讲并不陌生,它是灵活使用react组件的一种技巧,高阶组件自己不是组件,它是一个参数为组件,返回值也是一个组件的函数。高阶做用用于强化组件,复用逻辑,提高渲染性能等做用。高阶组件也并非很难理解,其实接触事后仍是蛮简单的,接下来我将按照,高阶组件理解?高阶组件具体怎么使用?应用场景高阶组件实践(源码级别) 为突破口,带你们详细了解一下高阶组件。本文篇幅比较长,建议收藏观看前端

咱们带着问题去开始今天的讨论:vue

  • 1 什么是高阶组件,它解决了什么问题?
  • 2 有几种高阶组件,它们优缺点是什么?
  • 3 如何写一个优秀高阶组件?
  • 4 hoc怎么处理静态属性,跨层级ref等问题?
  • 5 高阶组件怎么控制渲染,隔离渲染?
  • 6 高阶组件怎么监控原始组件的状态?
  • ...

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而造成的设计模式。node

NAOTU.jpg

二 全方位看高阶组件

1 几种包装强化组件的方式

① mixin模式

原型图react

C32587B9-D0FB-46CA-9AF8-FE2DF49021E5.jpg

老版本的react-mixins

react初期提供一种组合方法。经过React.createClass,加入mixins属性,具体用法和vuemixins类似。具体实现以下。es6

const customMixin = {
  componentDidMount(){
    console.log( '------componentDidMount------' )
  },
  say(){
    console.log(this.state.name)
  }
}

const APP = React.createClass({
  mixins: [ customMixin ],
  getInitialState(){
    return {
      name:'alien'
    }
  },
  render(){
    const { name  } = this.state
    return <div> hello ,world , my name is { name } </div>
  }
})

复制代码

这种mixins只能存在createClass中,后来React.createClass连同mixins这种模式被废弃了。mixins会带来一些负面的影响。redux

  • 1 mixin引入了隐式依赖关系。
  • 2 不一样mixins之间可能会有前后顺序甚至代码冲突覆盖的问题
  • 3 mixin代码会致使滚雪球式的复杂性

衍生方式

createClass的废弃,不表明mixin模式退出react舞台,在有状态组件class,咱们能够经过原型链继承来实现mixins设计模式

const customMixin = {  /* 自定义 mixins */
  componentDidMount(){
    console.log( '------componentDidMount------' )
  },
  say(){
    console.log(this.state.name)
  }
}

function componentClassMixins(Component,mixin){ /* 继承 */
  for(let key in mixin){
    Component.prototype[key] = mixin[key]
  }
}

class Index extends React.Component{
  constructor(){
    super()
    this.state={  name:'alien' }
  }
  render(){
    return <div> hello,world <button onClick={ this.say.bind(this) } > to say </button> </div>
  }
}
componentClassMixins(Index,customMixin)
复制代码

②extends继承模式

原型图api

9F743F44-D7FD-4F81-805B-80E8D5A358DB.jpg

class组件盛行以后,咱们能够经过继承的方式进一步的强化咱们的组件。这种模式的好处在于,能够封装基础功能组件,而后根据须要去extends咱们的基础组件,按需强化组件,可是值得注意的是,必需要对基础组件有足够的掌握,不然会形成一些列意想不到的状况发生。数组

class Base extends React.Component{
  constructor(){
    super()
    this.state={
      name:'alien'
    }
  }
  say(){
    console.log('base components')
  }
  render(){
    return <div> hello,world <button onClick={ this.say.bind(this) } >点击</button> </div>
  }
}
class Index extends Base{
  componentDidMount(){
    console.log( this.state.name )
  }
  say(){ /* 会覆盖基类中的 say */
    console.log('extends components')
  }
}
export default Index
复制代码

③HOC模式

原型图缓存

4F67D3DC-3B06-4B05-A006-B653D736855B.jpg

HOC是咱们本章主要的讲的内容,具体用法,咱们接下来会慢慢道来,咱们先简单尝试一个HOC

function HOC(Component) {
  return class wrapComponent extends React.Component{
     constructor(){
       super()
       this.state={
         name:'alien'
       }
     }
     render=()=><Component { ...this.props } { ...this.state } />
  }
}

@HOC
class Index extends React.Component{
  say(){
    const { name } = this.props
    console.log(name)
  }
  render(){
    return <div> hello,world <button onClick={ this.say.bind(this) } >点击</button> </div>
  }
}
复制代码

④自定义hooks模式

原型图

1956EF23-7BFA-4003-9902-4D444B329290.jpg

hooks的诞生,一大部分缘由是解决无状态组件没有state逻辑难以复用问题。hooks能够将一段逻辑封装起来,作到开箱即用,我这里就很少讲了,接下来会出react-hooks原理的文章,完成react-hooks三部曲。感兴趣的同窗能够看笔者的另外二篇文章,里面详细介绍了react-hooks复用代码逻辑的原则和方案。

传送门:

玩转react-hooks,自定义hooks设计模式及其实战

react-hooks如何使用?

2 高阶组件产生初衷

组件是把prop渲染成UI,而高阶组件是将组件转换成另一个组件,咱们更应该注意的是,通过包装后的组件,得到了那些强化,节省多少逻辑,或是解决了原有组件的那些缺陷,这就是高阶组件的意义。咱们先来思考一下高阶组件究竟解决了什么问题🤔🤔🤔?

① 复用逻辑:高阶组件更像是一个加工react组件的工厂,批量对原有组件进行加工包装处理。咱们能够根据业务需求定制化专属的HOC,这样能够解决复用逻辑。

② 强化props:这个是HOC最经常使用的用法之一,高阶组件返回的组件,能够劫持上一层传过来的props,而后混入新的props,来加强组件的功能。表明做react-router中的withRouter

③ 赋能组件HOC有一项独特的特性,就是能够给被HOC包裹的业务组件,提供一些拓展功能,好比说额外的生命周期,额外的事件,可是这种HOC,可能须要和业务组件紧密结合。典型案例react-keepalive-router中的 keepaliveLifeCycle就是经过HOC方式,给业务组件增长了额外的生命周期。

④ 控制渲染:劫持渲染是hoc一个特性,在wrapComponent包装组件中,能够对原来的组件,进行条件渲染节流渲染懒加载等功能,后面会详细讲解,典型表明作react-reduxconnectdvadynamic 组件懒加载。

我会针对高阶组件的初衷展开,详细介绍其原理已经用法。跟上个人思路,咱们先来看一下,高阶组件如何在咱们的业务组件中使用的

3 高阶组件使用和编写结构

HOC使用指南是很是简单的,只须要将咱们的组件进行包裹就能够了。

使用:装饰器模式和函数包裹模式

对于class声明的有状态组件,咱们能够用装饰器模式,对类组件进行包装:

@withStyles(styles)
@withRouter
@keepaliveLifeCycle
class Index extends React.Componen{
    /* ... */
}
复制代码

咱们要注意一下包装顺序,越靠近Index组件的,就是越内层的HOC,离组件Index也就越近。

对于无状态组件(函数声明)咱们能够这么写:

function Index(){
    /* .... */
}
export default withStyles(styles)(withRouter( keepaliveLifeCycle(Index) )) 
复制代码

模型:嵌套HOC

对于不须要传递参数的HOC,咱们编写模型咱们只须要嵌套一层就能够,好比withRouter,

function withRouter(){
    return class wrapComponent extends React.Component{
        /* 编写逻辑 */
    }
}

复制代码

对于须要参数的HOC,咱们须要一层代理,以下:

function connect (mapStateToProps){
    /* 接受第一个参数 */
    return function connectAdvance(wrapCompoent){
        /* 接受组件 */
        return class WrapComponent extends React.Component{  }
    }
}

复制代码

咱们看出两种hoc模型很简单,对于代理函数,可能有一层,可能有不少层,不过不要怕,不管多少层本质上都是同样的,咱们只须要一层一层剥离开,分析结构,整个hoc结构和脉络就会清晰可见。吃透hoc也就易如反掌。

4 两种不一样的高阶组件

经常使用的高阶组件有两种方式正向的属性代理反向的组件继承,二者以前有一些共性和区别。接下具体介绍二者区别,在第三部分会详细介绍具体实现。

正向属性代理

所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,咱们能够作一些,对源组件的代理操做。在fiber tree 上,先mounted代理组件,而后才是咱们的业务组件。咱们能够理解为父子组件关系,父组件对子组件进行一系列强化操做。

function HOC(WrapComponent){
    return class Advance extends React.Component{
       state={
           name:'alien'
       }
       render(){
           return <WrapComponent { ...this.props } { ...this.state } />
       }
    }
}
复制代码

优势

  • ① 正常属性代理能够和业务组件低耦合,零耦合,对于条件渲染props属性加强,只负责控制子组件渲染和传递额外的props就能够,因此无须知道,业务组件作了些什么。因此正向属性代理,更适合作一些开源项目的hoc,目前开源的HOC基本都是经过这个模式实现的。
  • ② 一样适用于class声明组件,和function声明的组件。
  • ③ 能够彻底隔离业务组件的渲染,相比反向继承,属性代理这种模式。能够彻底控制业务组件渲染与否,能够避免反向继承带来一些反作用,好比生命周期的执行。
  • ④ 能够嵌套使用,多个hoc是能够嵌套使用的,并且通常不会限制包装HOC的前后顺序。

缺点

  • ① 通常没法直接获取业务组件的状态,若是想要获取,须要ref获取组件实例。

  • ② 没法直接继承静态属性。若是须要继承须要手动处理,或者引入第三方库。

例子:

class Index extends React.Component{
  render(){
    return <div> hello,world </div>
  }
}
Index.say = function(){
  console.log('my name is alien')
}
function HOC(Component) {
  return class wrapComponent extends React.Component{
     render(){
       return <Component { ...this.props } { ...this.state } />
     }
  }
}
const newIndex =  HOC(Index) 
console.log(newIndex.say)

复制代码

打印结果

29B0DA43-A037-473C-AD76-6550A3849CE8.jpg

反向继承

反向继承和属性代理有必定的区别,在于包装后的组件继承了业务组件自己,因此咱们我无须在去实例化咱们的业务组件。当前高阶组件就是继承后,增强型的业务组件。这种方式相似于组件的强化,因此你必要要知道当前

class Index extends React.Component{
  render(){
    return <div> hello,world </div>
  }
}
function HOC(Component){
    return class wrapComponent extends Component{ /* 直接继承须要包装的组件 */

    }
}
export default HOC(Index) 
复制代码

优势

  • ① 方便获取组件内部状态,好比stateprops ,生命周期,绑定的事件函数等
  • es6继承能够良好继承静态属性。咱们无须对静态属性和方法进行额外的处理。
class Index extends React.Component{
  render(){
    return <div> hello,world </div>
  }
}
Index.say = function(){
  console.log('my name is alien')
}
function HOC(Component) {
  return class wrapComponent extends Component{
  }
}
const newIndex =  HOC(Index) 
console.log(newIndex.say)
复制代码

打印结果

3618DB30-8D9F-445A-8A01-69076A0B1E1D.jpg

缺点

  • ① 无状态组件没法使用。
  • ② 和被包装的组件强耦合,须要知道被包装的组件的内部状态,具体是作什么?
  • ③ 若是多个反向继承hoc嵌套在一块儿,当前状态会覆盖上一个状态。这样带来的隐患是很是大的,好比说有多个componentDidMount,当前componentDidMount会覆盖上一个componentDidMount。这样反作用串联起来,影响很大。

三 如何编写高阶组件

接下来咱们来看看,如何编写一个高阶组件,你能够参考以下的情景,去编写属于本身的HOC

1 强化props

① 混入props

这个是高阶组件最经常使用的功能,承接上层的props,在混入本身的props,来强化组件。

有状态组件(属性代理)

function classHOC(WrapComponent){
    return class Idex extends React.Component{
        state={
            name:'alien'
        }
        componentDidMount(){
           console.log('HOC')
        }
        render(){
            return <WrapComponent { ...this.props } { ...this.state } />
        }
    }
}
function Index(props){
  const { name } = props
  useEffect(()=>{
     console.log( 'index' )
  },[])
  return <div> hello,world , my name is { name } </div>
}

export default classHOC(Index)
复制代码

有状态组件(属性代理)

一样也适用与无状态组件。

function functionHoc(WrapComponent){
    return function Index(props){
        const [ state , setState ] = useState({ name :'alien'  })       
        return  <WrapComponent { ...props } { ...state } />
    }
}
复制代码

效果

A6FC09B4-EAA0-4A5A-BA3A-F7F2A8407C75.jpg

② 抽离state控制更新

高阶组件能够将HOCstate的配合起来,控制业务组件的更新。这种用法在react-reduxconnect高阶组件中用到过,用于处理来自reduxstate更改,带来的订阅更新做用。

咱们将上述代码进行改造。

function classHOC(WrapComponent){
  return class Idex extends React.Component{
      constructor(){
        super()
        this.state={
          name:'alien'
        }
      }
      changeName(name){
        this.setState({ name })
      }
      render(){
          return <WrapComponent { ...this.props } { ...this.state } changeName={this.changeName.bind(this) } />
      }
  }
}
function Index(props){
  const [ value ,setValue ] = useState(null)
  const { name ,changeName } = props
  return <div> <div> hello,world , my name is { name }</div> 改变name <input onChange={ (e)=> setValue(e.target.value) } /> <button onClick={ ()=> changeName(value) } >肯定</button> </div>
}

export default classHOC(Index)
复制代码

效果

屏幕录制2021-03-13 下午6.gif

2 控制渲染

控制渲染是高阶组件的一个很重要的特性,上边说到的两种高阶组件,都能完成对组件渲染的控制。具体实现仍是有区别的,咱们一块儿来探索一下。

2.1 条件渲染

① 基础 :动态渲染

对于属性代理的高阶组件,虽然不能在内部操控渲染状态,可是能够在外层控制当前组件是否渲染,这种状况应用于,权限隔离懒加载延时加载等场景。

实现一个动态挂载组件的HOC

function renderHOC(WrapComponent){
  return class Index extends React.Component{
      constructor(props){
        super(props)
        this.state={ visible:true }  
      }
      setVisible(){
         this.setState({ visible:!this.state.visible })
      }
      render(){
         const {  visible } = this.state 
         return <div className="box" > <button onClick={ this.setVisible.bind(this) } > 挂载组件 </button> { visible ? <WrapComponent { ...this.props } setVisible={ this.setVisible.bind(this) } /> : <div className="icon" ><SyncOutlined spin className="theicon" /></div> } </div>
      }
  }
}

class Index extends React.Component{
  render(){
    const { setVisible } = this.props
    return <div className="box" > <p>hello,my name is alien</p> <img src='https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&fm=26&gp=0.jpg' /> <button onClick={() => setVisible()} > 卸载当前组件 </button> </div>
  }
}
export default renderHOC(Index)
复制代码

效果:

屏幕录制2021-03-13 下午9.gif

② 进阶 :分片渲染

是否是感受不是很过瘾,为了让你们增强对HOC条件渲染的理解,我再作一个分片渲染+懒加载功能。为了让你们明白,我也是绞尽脑汁啊😂😂😂。

进阶:实现一个懒加载功能的HOC,能够实现组件的分片渲染,用于分片渲染页面,不至于一次渲染大量组件形成白屏效果

const renderQueue = []
let isFirstrender = false

const tryRender = ()=>{
  const render = renderQueue.shift()
  if(!render) return
  setTimeout(()=>{
    render() /* 执行下一段渲染 */
  },300)
} 
/* HOC */
function renderHOC(WrapComponent){
    return function Index(props){
      const [ isRender , setRender ] = useState(false)
      useEffect(()=>{
        renderQueue.push(()=>{  /* 放入待渲染队列中 */
          setRender(true)
        })
        if(!isFirstrender) {
          tryRender() /**/
          isFirstrender = true
        }
      },[])
      return isRender ? <WrapComponent tryRender={tryRender} { ...props } /> : <div className='box' ><div className="icon" ><SyncOutlined spin /></div></div>
    }
}
/* 业务组件 */
class Index extends React.Component{
  componentDidMount(){
    const { name , tryRender} = this.props
    /* 上一部分渲染完毕,进行下一部分渲染 */
    tryRender()
    console.log( name+'渲染')
  }
  render(){
    return <div> <img src="https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=294206908,2427609994&amp;fm=26&amp;gp=0.jpg" /> </div>
  }
}
/* 高阶组件包裹 */
const Item = renderHOC(Index)

export default () => {
  return <React.Fragment> <Item name="组件一" /> <Item name="组件二" /> <Item name="组件三" /> </React.Fragment>
}
复制代码

效果

fenload.gif

大体流程,初始化的时候,HOC中将渲染真正组件的渲染函数,放入renderQueue队列中,而后初始化渲染一次,接下来,每个项目组件,完成 didMounted 状态后,会从队列中取出下一个渲染函数,渲染下一个组件, 一直到全部的渲染任务所有执行完毕,渲染队列清空,有效的进行分片的渲染,这种方式对海量数据展现,很奏效。

HOC实现了条件渲染-分片渲染的功能,实际条件渲染理解起来很容易,就是经过变量,控制是否挂载组件,从而知足项目自己需求,条件渲染能够演变成不少模式,我这里介绍了条件渲染的二种方式,但愿你们可以理解精髓所在。

③ 进阶:异步组件(懒加载)

不知道你们有没有用过dva,里面的dynamic就是应用HOC模式实现的组件异步加载,我这里简化了一下,提炼核心代码,以下:

/* 路由懒加载HOC */
export default function AsyncRouter(loadRouter) {
  return class Content extends React.Component {
    state = {Component: null}
    componentDidMount() {
      if (this.state.Component) return
      loadRouter()
        .then(module => module.default)
        .then(Component => this.setState({Component},
         ))
    }
    render() {
      const {Component} = this.state
      return Component ? <Component { ...this.props } /> : null
    }
  }
}
复制代码

使用

const Index = AsyncRouter(()=>import('../pages/index'))
复制代码

hoc还能够配合其余API,作一下衍生的功能。如上配合import实现异步加载功能。HOC用起来很是灵活,

④ 反向继承 : 渲染劫持

HOC反向继承模式,能够实现颗粒化的渲染劫持,也就是能够控制基类组件的render函数,还能够篡改props,或者是children,咱们接下来看看,这种状态下,怎么使用高阶组件。

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

复制代码

⑤ 反向继承:修改渲染树

修改渲染状态(劫持render替换子节点)

class Index extends React.Component{
  render(){
    return <div> <ul> <li>react</li> <li>vue</li> <li>Angular</li> </ul> </div>
  }
}

function HOC (Component){
  return class Advance extends Component {
    render() {
      const element = super.render()
      const otherProps = {
        name:'alien'
      }
      /* 替换 Angular 元素节点 */
      const appendElement = React.createElement('li' ,{} , `hello ,world , my name 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)
    }
  }
}
export  default HOC(Index)

复制代码

效果

40D6BF30-9B4C-4EC9-B089-1E757DAC15DF.jpg

咱们用劫持渲染的方式,来操纵super.render()后的React.element元素,而后配合 createElement , cloneElement , React.Childrenapi,能够灵活操纵,真正的渲染react.element,能够说是偷天换日,不亦乐乎。

2.2节流渲染

hoc除了能够进行条件渲染渲染劫持功能外,还能够进行节流渲染,也就是能够优化性能,具体怎么作,请跟上个人节奏往下看。

① 基础: 节流原理

hoc能够配合hooksuseMemoAPI配合使用,能够实现对业务组件的渲染控制,减小渲染次数,从而达到优化性能的效果。以下案例,咱们指望当且仅当num改变的时候,渲染组件,可是不影响接收的props。咱们应该这样写咱们的HOC

function HOC (Component){
     return function renderWrapComponent(props){
       const { num } = props
       const RenderElement = useMemo(() =>  <Component {...props} /> ,[ num ])
       return RenderElement
     }
}
class Index extends React.Component{
  render(){
     console.log(`当前组件是否渲染`,this.props)
     return <div>hello,world, my name is alien </div>
  }
}
const IndexHoc = HOC(Index)

export default ()=> {
    const [ num ,setNumber ] = useState(0)
    const [ num1 ,setNumber1 ] = useState(0)
    const [ num2 ,setNumber2 ] = useState(0)
    return <div> <IndexHoc num={ num } num1={num1} num2={ num2 } /> <button onClick={() => setNumber(num + 1) } >num++</button> <button onClick={() => setNumber1(num1 + 1) } >num1++</button> <button onClick={() => setNumber2(num2 + 1) } >num2++</button> </div>
}
复制代码

效果:

rend1.gif

如图所示,当咱们只有点击 num++时候,才从新渲染子组件,点击其余按钮,只是负责传递了props,达到了指望的效果。

② 进阶:定制化渲染流

思考:🤔上述的案例只是介绍了原理,在实际项目中,是量化生产不了的,缘由是,咱们须要针对不一样props变化,写不一样的HOC组件,这样根本起不了Hoc真正的用途,也就是HOC产生的初衷。因此咱们须要对上述hoc进行改造升级,是组件能够根据定制化方向,去渲染组件。也就是Hoc生成的时候,已经按照某种契约去执行渲染。

function HOC (rule){
     return function (Component){
        return function renderWrapComponent(props){
          const dep = rule(props)
          const RenderElement = useMemo(() =>  <Component {...props} /> ,[ dep ])
          return RenderElement
        }
     }
}
/* 只有 props 中 num 变化 ,渲染组件 */
@HOC( (props)=> props['num'])
class IndexHoc extends React.Component{
  render(){
     console.log(`组件一渲染`,this.props)
     return <div> 组件一 : hello,world </div>
  }
}

/* 只有 props 中 num1 变化 ,渲染组件 */
@HOC((props)=> props['num1'])
class IndexHoc1 extends React.Component{
  render(){
     console.log(`组件二渲染`,this.props)
     return <div> 组件二 : my name is alien </div>
  }
}
export default ()=> {
    const [ num ,setNumber ] = useState(0)
    const [ num1 ,setNumber1 ] = useState(0)
    const [ num2 ,setNumber2 ] = useState(0)
    return <div> <IndexHoc num={ num } num1={num1} num2={ num2 } /> <IndexHoc1 num={ num } num1={num1} num2={ num2 } /> <button onClick={() => setNumber(num + 1) } >num++</button> <button onClick={() => setNumber1(num1 + 1) } >num1++</button> <button onClick={() => setNumber2(num2 + 1) } >num2++</button> </div>
}
复制代码

效果

hoc2.gif

完美实现了效果。这用高阶组件模式,能够灵活控制React组件层面上的,props数据流更新流,优秀的高阶组件有 mobxobserver ,inject , react-redux中的connect,感兴趣的同窗,能够抽时间研究一下。

3 赋能组件

高阶组件除了上述两种功能以外,还能够赋能组件,好比加一些额外生命周期劫持事件监控日志等等。

3.1 劫持原型链-劫持生命周期,事件函数

① 属性代理实现

function HOC (Component){
  const proDidMount = Component.prototype.componentDidMount 
  Component.prototype.componentDidMount = function(){
     console.log('劫持生命周期:componentDidMount')
     proDidMount.call(this)
  }
  return class wrapComponent extends React.Component{
      render(){
        return <Component {...this.props} />
      }
  }
}
@HOC
class Index extends React.Component{
   componentDidMount(){
     console.log('———didMounted———')
   }
   render(){
     return <div>hello,world</div>
   }
}
复制代码

效果

A04A37C8-71CF-4DFD-BD59-E741DCC35EF4.jpg

② 反向继承实现

反向继承,由于在继承原有组件的基础上,能够对原有组件的生命周期事件进行劫持,甚至是替换。

function HOC (Component){
  const didMount = Component.prototype.componentDidMount
  return class wrapComponent extends Component{
      componentDidMount(){
        console.log('------劫持生命周期------')
        if (didMount) {
           didMount.apply(this) /* 注意 `this` 指向问题。 */
        }
      }
      render(){
        return super.render()
      }
  }
}

@HOC
class Index extends React.Component{
   componentDidMount(){
     console.log('———didMounted———')
   }
   render(){
     return <div>hello,world</div>
   }
}
复制代码

3.2 事件监控

HOC还能够对原有组件进行监控。好比对一些事件监控错误监控事件监听等一系列操做。

① 组件内的事件监听

接下来,咱们作一个HOC,只对组件内的点击事件作一个监听效果。

function ClickHoc (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>
  }
}

@ClickHoc
class Index extends React.Component{
   render(){
     return <div className='index' > <p>hello,world</p> <button>组件内部点击</button> </div>
   }
}
export default ()=>{
  return <div className='box' > <Index /> <button>组件外部点击</button> </div>
}
复制代码

效果

click.gif

3 ref助力操控组件实例

对于属性代理咱们虽然不能直接获取组件内的状态,可是咱们能够经过ref获取组件实例,获取到组件实例,就能够获取组件的一些状态,或是手动触发一些事件,进一步强化组件,可是注意的是:class声明的有状态组件才有实例,function声明的无状态组件不存在实例。

① 属性代理-添加额外生命周期

咱们能够针对某一种状况, 给组件增长额外的生命周期,我作了一个简单的demo,监听number改变,若是number改变,就自动触发组件的监听函数handerNumberChange。 具体写法以下

function Hoc(Component){
  return class WrapComponent extends React.Component{
      constructor(){
        super()
        this.node = null
      }
      UNSAFE_componentWillReceiveProps(nextprops){
          if(nextprops.number !== this.props.number ){
            this.node.handerNumberChange  &&  this.node.handerNumberChange.call(this.node)
          }
      }
      render(){
        return <Component {...this.props} ref={(node) => this.node = node } />
      }
  }
}
@Hoc
class Index extends React.Component{
  handerNumberChange(){
      /* 监听 number 改变 */
  }
  render(){
    return <div>hello,world</div>
  }
}
复制代码

这种写法有点不尽人意,你们不要着急,在第四部分,源码实战中,我会介绍一种更好的场景。方便你们理解Hoc对原有组件的赋能。

4 总结

上面我分别按照hoc主要功能,强化props控制渲染赋能组件 三个方向对HOC编写作了一个详细介绍,和应用场景的介绍,目的让你们在理解高阶组件的时候,更明白何时会用到?,怎么样去写?` 里面涵盖的知识点我总一个总结。

对于属性代理HOC,咱们能够:

  • 强化props & 抽离state。
  • 条件渲染,控制渲染,分片渲染,懒加载。
  • 劫持事件和生命周期
  • ref控制组件实例
  • 添加事件监听器,日志

对于反向代理的HOC,咱们能够:

  • 劫持渲染,操纵渲染树
  • 控制/替换生命周期,直接获取组件状态,绑定事件。

每一个应用场景,我都举了例子🌰🌰,你们能够结合例子深刻了解一下其原理和用途。

四 高阶组件源码级实践

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中文文档

相关文章
相关标签/搜索