react之HOC高阶组件

914 阅读7分钟
一、什么是hoc

​ hoc(高阶组件)是react中的一种组织代码的手段,而不是一个api,这种设计模式可以复用在react组件中的代码与逻辑,因为一般来说react组件比较容易复用渲染函数,也就是主要负责html的输出.

​ 高阶组件实际上是经过以一个包装函数返回的组件,这类函数接受react组件处理传入的组件,然后返回一个新的组件。

二、如何实现一个hoc
  1. 找出组件中复用的逻辑
  2. 创建适用于上方逻辑的函数
  3. 利用这个函数来创建一个组件
  4. 调用
2.1找出组件中复用的逻辑
  1. 组件创建
  2. 向服务器拉去数据
  3. 利用数据渲染组件
  4. 监听数据的变化
  5. 数据变化或者处罚修改的事件
  6. 利用变化后的数据再次渲染
  7. 组件销毁移除监听的数据源
三、函数约定
1.hoc函数不要讲多余的props传递给被包裹对的组件

​ hoc函数需要像透明的一样,经过他的包裹产生新的组件和传入前没有什么区别。这样做的目的在于,我们不需要考虑经过hoc函数对传入的组件进行了修改,那么套用这种hoc含糊是多次后返回的组件在使用的时候,不得不考虑这个组件带来的一些非预期行为。

2.hoc是函数!利用函数来最大化组合性

​ 因为hoc是一个返回组件的函数,只要是函数可以做的事hoc同样可以做到,利用这一点,我们可以借用在使用react之前我们就学过的一些东西,例:定义一个高阶函数用于返回一个高阶组件,或者干脆是一个不接受任何参数的函数。

四、注意
1、不要在render方法中使用hoc

​ 我们都知道react会调用render方法来渲染组件,当然react也会做一些额外的工作例如性能优化,在组件重新渲染的时候react会判断当前render返回的组件和未之前的组件是否相等 === 如果相等react会递归更新组件,反之他会彻底的卸载之前的旧的本案本来渲染当前的组件。因为diff算法使用组件标识来确定是否应该更新子树还是丢弃并挂载新子树,如果返回的组件与前一个组件相同,则递归更新新子树,如果不相等,则会完全卸载前一个子树,而重新挂载组件会导致该组件及其所有子组件的状态丢失。

2、记得复制静态方法

react的组件一般是继承React.Componment的子类.

​ 不要忘记了一个类除了实力方法外还有静态方法,使用hoc我们对组件进行了一层包装会覆盖掉原来的静态方法:

class Demo extends React.Compoent{
  render(){
   return (
     <div>{this.props.children}</div>
   )
  }
}

Demo.echo = function(){
  console.log('hello world')
}

Demo.echo();

function DemoHoc(Wrap){
  return class extends React.Componet{
    render(){
      return (
         <Wrap>{'hello world'}</Wrap>
      )
    }
  }
}

const App = DemoHoc(Demo);

App.echo();

解决方式

在hoc内部直接将原来组件的静态方法复制就可以了:

function DemoHoc(Wrap){
  const myClass = class extens React.Componet{
    render(){
       return (
         <Wrap>{'hello world'}</Wrap>
       )
    }
  }
  
  myClass.echo = Wrap.echo;
  return myClass;
}

不过这样一来hoc中就需要知道被复制的静态方法名是什么,结合之前提到的灵活使用HOC我们可以让HOC接收方法参数名称:

function DemoHoc(Wrap,staticMethods=[]){             //默认给空数组
  const myClass = class extends ReaCT.Componet{
    redner(){
      return (
        <Wrap>{'hello world'}</Wrap>
      )
    }
  }

for(const metyhodName of staticMethods){
  myClass[methodsName] = Wrap[methodsName];
}
return myClass;
}

const App = DemoHoc(Demo,['echo']);
3.透传 ref

ref作为组件上的特殊属性,无法像普通的props那样被向下传递。

class Wraped extends React.Componet{
  constructor(props){
    super(props);
    this.state = {
      message:''
    }
  }
  echo(){
    this.setState({
       message:'hell world'
    })
  }
  
  render(){
     return <div>{this.state.message}</div>
  }
}

我们使用一个HOC包裹它:

function ExampleHoc(Wrap){
  return class extends React.Compoent{
    render(){
      return <Wrap></Wrap>
    }
  }
}

const Example = ExampleHoc(Wraped);

现在我们把这个组件放入到App组件中进行渲染,并且使用ref来引用这个返回的组件,并试图调用它的echo方法:

const ref = React.createRef();

class App Extends React.Component{
   handleEcho = () =>{
     ref.current.echo();
   }
   render(){
     return (
        <div>
          <Example ref={ref}></Example>
          <button onClick={this.handleEcho}>echo</button>
        </div>
     )
   }
}

​ 但是当你点击按钮试图触发子组件的事件的时候他不起作用,系统报错没echo方法。实际上ref被绑定到了HOC返回的那个匿名类上,想要绑定到内部的组件中我们可以进行ref透传,默认的情况下ref是无法被进行向下 传递的因为ref是特殊的属性就和key一样不会被添加到props中,因此react提供了一个API来实现透传ref的这种需求。

这个api就是React.forwardRef. 这个方法接受一个函数返回一个组件,在这个函数中它可以读取到组件传入的ref,某种意义上React.forwardRef也相当于一个高阶组件:

const ReturnedCompoent  =React.forwardRef((props,ref) =>{
  //我们可以获取到在props中无法获取的ref属性了.
  return;   // 返回这个需要使用ref属性的组件          
})

我们把这个api用在之前的HOC中:

function Example(Wrap){
   class Inner extends React.Compoent{
     render(){
       const {forwardRef,...rest} = this.props;
       return <Wrap ref={forwardedRef} {...rest}></Wrap> // 我们接收到props中被改名的ref然后绑定到ref上
     }
   }
   return React.forwardRef((props,ref) =>{    //我们接收到ref然后给他改名称forwardedRef传入到props中
     return <Inner {..props} forwardedRef={ref}></Inner>
   })
}

这个时候在调用echo就没有问题了:

handleEcho = () =>{
  ref.current.echo();
}
五、高阶组件的实现方式
1、属性代理

​ 在外部通过属性传值的方法给被高阶组件加工后的组件传值的时候,值会传给高阶组件,而不是被加工组件,换句话说,this.props只能在高阶组件内取到值。如果想在被加工组件内通过this.props取值,需要高阶组件往被加工组件身上传值。这就是属性代理。

通过属性代理的方式,可以使高阶组件使用起来更加自由,做更多的事情。

在app组件内调用Home组件渲染时,传递一个值:

import React from 'react'
import Home from './components/home'
export default class App extends React.Component{
    render(){
        return (
            <div>
                {/* 在此向Home组件传值 */}
                <Home user="haha"/>
            </div>
        )
    }
}

在Home组件通过this.props进行接收,结果为空对象,没有接收到值:

// ... //    
render(){
    console.log(this.props)  // 输出结果为:{}
    return (
        <div>我是组件</div>
    )
}
// ... //

在高阶组件内通过this.props进行接收,接收成功:

//...        
render(){
    console.log(this.props) // 输出结果为:{user:"haha"}
    return (
        <Fragment>
            {this.state.isHeader?<div>Header</div>:""}
            <WrapperComponent />
        </Fragment>
    )
}
// ...
2.反向继承

在高阶组件中,因为返回的组件直接继承了被加工组件,所以可以直接通过this.props访问被加工组件的数据。

1.在被加工组件Home中,定义数据isHeader,来控制是否显示Header

import React,{Component} from 'react'
import layout from '../layout'

class Home extends Component{
    constructor(){
        super();
        // 在此处定义自己的数据
        this.state = {
            isHeader:true
        }
    }
    render(){
        return (
         <div>我是组件</div>
        )
    }
}
export default layout(Home);
  1. 在高阶组件中,直接通过this.state获取Home组件中的state数据。
import React,{ Fragment } from 'react

export default (WrapperComponent)=>{
    return class extends WrapperComponent{
        render(){
            return (
             <Fragment>
                    {/* 在此处访问被加工组件的数据,如果isHeader为true,则渲染Header */}
                 {this.state.isHeader?<div>Header</div>:""}
                    <WrapperComponent />
                </Fragment>
            )
        }
    }
}

高阶组件继承了Home组件的所有东西,自然可以通过this来访问被加工的组件的属性了。

高阶组件的super

在高阶组件中,通过反向继承被加工的组件之后,可以对被继承组件做很多操作,例如通过super访问被继承组件中的方法。

super渲染被继承组件

在高阶组件中,渲染被继承组件有两种方法:

1、通过标签名直接渲染:

export default (WrapperComponent)=>{
    return class extends WrapperComponent{
        render(){
            return (
             <div>
                    <WrapperComponent />
                </div>
            )
        }
    }
}

2.通过super.redner()调用被继承组件的redner函数进行渲染

export default (WrapperComponent)=>{
    return class extends WrapperComponent{
        render(){
            return (
             <div>
                    {super.render()}
                </div>
            )
        }
    }
}

通过super调用被继承组件的方法

1、被继承组件中定义方法和数据:

import React, { Component } from 'react'
import layout from '../layout'

class Home extends Component {
    constructor() {
        super();
        this.state = {
            componentName: "Home组件"
        }
    }
    render() {
        return (
            <div>我是组件</div>
        )
    }
    handleClick() {
        alert(1);
    }
    componentDidMount() {
        console.log("componentDidMount")
    }
}

// 在导出组件的时候,用高阶组件进行加工,则外部获取的组件便是加工后的有Header的组件
export default layout(Home);

2、在高阶组件中通过super进行操作

import React,{ Fragment } from 'react'

export default (WrapperComponent)=>{
    return class extends WrapperComponent{
        render(){
            console.log(this.props,"layout")
            return (
             <div>
                    {super.render()}
                    {/* 操作被继承组件的方法,点击后成功alert(1) */}
                    <button onClick={super.handleClick.bind(this)}>点击</button>
                </div>
            )
        }
        componentDidMount(){
            console.log("abc")       // 输出结果:abc
            {/* 操作被继承组件的生命周期 */}
            super.componentDidMount() // 输出结果:componentDidMount
        }
    }
}