React学习笔记 --- 高阶组件和组件补充

585 阅读10分钟

一、 高阶函数和高阶组件

1.1 高阶函数

高阶函数的维基百科定义:至少满足以下条件之一:

  1. 接受一个或多个函数作为输入;
  2. 输出一个函数;

JavaScript中比较常见的filter、map、reduce都是高阶函数。

1.2 高阶组件

那么什么是高阶组件呢?

  1. 高阶组件的英文是 Higher-Order Components,简称为 HOC

  2. 官方的定义:高阶组件是参数为组件,返回值为新组件的函数

我们可以进行如下的解析:

  1. 首先, 高阶组件 本身不是一个组件,而是一个函数;
  2. 其次,这个函数的参数是一个组件,返回值也是一个组件

w0Ubgx.png

示例

import React, { PureComponent } from 'react'

class App extends PureComponent {
  render() {
    return (
      <div>
        App
      </div>
    )
  }
}

// 这就是一个高阶函数
// 因为传入的参数是一个组件对象,所以形参首字母需要大写
function enhanceComponent(WrappedComponent) {
  // 高阶组件返回的是一个新组件
  return class HighOrderComponent extends PureComponent{
    render() {
      return <WrappedComponent />
    }
  }
}

// 调用高阶组件
// 注意传入的是一个新的组件对象 所以是App
// 不是ReactElement 不要写成<App />
export default enhanceComponent(App)

w0afzt.png

省略组件名称

在传统的函数书写中

// 函数声明式
function fun() { .... }

// 函数表达式
// 此时function后面的fun是没有任何的意义的
const fun = function fun() { ... }

// 所以 可以简化为
const fun = function() {...}

同样的道理,在类的使用和定义中

class Person{ ... }

// 转换为类的表达式
cosnt Person = class Person { ... }

// 此时class后面的Person也是没有任何的价值的
// 可以简写为
const Person = calss Person {...}

所以在高阶组件的编写中,可以修改为

import React, { PureComponent } from 'react'

class App extends PureComponent {
  render() {
    return (
      <div>
        App
      </div>
    )
  }
}

function enhanceComponent(WrappedComponent) {
  // 这里可以省略类组件的名称
  return class extends PureComponent{
    render() {
      return <WrappedComponent />
    }
  }
}

export default enhanceComponent(App)

w0wwDK.png

为组件起别名

import React, { PureComponent } from 'react'

class App extends PureComponent {
  render() {
    return (
      <div>
        App
      </div>
    )
  }
}

function enhanceComponent(WrappedComponent) {
   class HighOrderComponent extends PureComponent{
    render() {
      return <WrappedComponent />
    }
  }

  // 任何一个react中的组件(无论是类组件还是函数组件)上都有一个属性叫做displayName 可以修改对应的组件名称
  HighOrderComponent.displayName = 'HOC'

  return HighOrderComponent
}

export default enhanceComponent(App)

w0w9jP.png

属性值的传递

index.js

ReactDOM.render(<App name="Klaus" />, document.getElementById('root'))

App.js

import React, { PureComponent } from 'react'

class App extends PureComponent {
  render() {
    return (
      <div>
        name: { this.props.name }
      </div>
    )
  }
}

function enhanceComponent(WrappedComponent) {
  return class extends PureComponent{
    // 使用展开的方式将父组件中的所有props传递给形参组件
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

export default enhanceComponent(App)

高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式

高阶组件在一些React第三方库中非常常见:

  1. 比如redux中的connect
  2. 比如react-router中的withRouter

高阶组件中使用函数组件

function enhanceComponent(WrappedComponent) {
  return function(props){
    return <WrappedComponent {...props} />
  }
}

二、 高阶组件的使用

props增强

不修改原有代码的情况下,添加新的props

w0RQkq.png

Home.js

import React, { PureComponent } from 'react'

export default class About extends PureComponent {
  render() {
    return (
      <div>
          name: {this.props.name}
          <br/>
          age: {this.props.age}
      </div>
    )
  }
}

About.js

import React, { PureComponent } from 'react'

export default class About extends PureComponent {
  render() {
    return (
      <div>
          name: {this.props.name}
          <br/>
          age: {this.props.age}
      </div>
    )
  }
}

App.js

import React, { PureComponent } from 'react'

import Home from './Home'
import About from './About'

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home name='Klaus' age={23} />
        <hr/>
        <About name="Steven" age={25} />
      </div>
    )
  }
}

需求: 为Home组件和About组件添加一个共有的属性,叫做region="china",

如果直接在组件上进行添加的话,那么就需要在所有使用Home组件和About组件的地方进行修改,

此时就可以region就可以被设置在HOC中

修改后的代码如下:

App.js

import React, { PureComponent } from 'react'

import Home from './Home'
import About from './About'

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <Home name='Klaus' age={23} />
        <hr/>
        <About name="Steven" age={25} />
      </div>
    )
  }
}

Home.js

import React, { PureComponent } from 'react'
import {enhanceProps} from './HOC.js'

class Home extends PureComponent {
  render() {
    return (
      <div>
          name: {this.props.name}
          <br/>
          age: {this.props.age}
          <br/>
          ergion: {this.props.region}
      </div>
    )
  }
}

// 导出的时候使用高阶函数对原有的组件进行劫持后进行二次操作
export default enhanceProps(Home)

About.js

import React, { PureComponent } from 'react'
import {enhanceProps} from './HOC.js'

class About extends PureComponent {
  render() {
    return (
      <div>
          name: {this.props.name}
          <br/>
          age: {this.props.age}
          <br/>
          ergion: {this.props.region}
      </div>
    )
  }
}

export default enhanceProps(About)

HOC.js

import React from 'react'

export function enhanceProps(Cpn) {
  return props => <Cpn {...props} region="china" />
}

共享context

可以利用高阶组件来共享Context,避免多次调用Consumer组件

App.js

import React, { PureComponent } from 'react'
import {UserCtx} from './context'

import Home from './Home'
import About from './About'

export default class App extends PureComponent {
  render() {
    return (
      <UserCtx.Provider value={{ name: 'Steven', age: 18 }}>
          {/*
          	如果使用了Provider 那么就会去取Provider中的value属性的值,不会去取默认值
          	但是如果没有提供Provider,那么就会去取默认属性的值
          */}
          <Home />
          <hr/>
          <About/>
      </UserCtx.Provider>
    )
  }
}

Home.js

import React, { PureComponent } from 'react'
import {withUser} from './HOC';

class Home extends PureComponent {
  render() {
    return (
      <div>
        name: {this.props.name}
        age: {this.props.age}
      </div>
    )
  }
}

export default withUser(Home)

About.js

import React, { PureComponent } from 'react'
import {withUser} from './HOC'

class About extends PureComponent {
  render() {
    return (
      <ul>
        <li>name: {this.props.name}</li>
        <li>age: {this.props.age}</li>
      </ul>
    )
  }
}

export default withUser(About)

HOC.js

import React from 'react'
import { UserCtx } from './context'

// 使用UserContext 所以取名为withUser
export function withUser(Cpn) {
  return props => (
    <UserCtx.Consumer>
       {
         user =><Cpn {...props} {...user} />
       }
    </UserCtx.Consumer>
  )
}

context.js

import { createContext } from 'react'

export const UserCtx = createContext({
  name: 'Klaus',
  age: 18
})

统一操作

在实际开发的时候,可能会需要进行一些相同的操作

例如: 发送相同的网络请求,加载动画,鉴权(权限鉴定)操作

import React, { PureComponent, Fragment } from 'react'

// 定义的是一个显示页面元素内容的时候, 起名为XXXPage
// 定义一个单纯的组件的时候,起名为XXXCpn
function LoginPage() {
  return <h2>LoginPage</h2>
}

function CartPage() {
  return <h2>CartPage</h2>
}

function ProfilePage() {
  return <h2>ProfilePage</h2>
}

// 鉴权操作
function withAuth(WrapCpn){
  const Auth =  props => {
    const { isLogin } = props

    return isLogin ? <WrapCpn {...props} /> : <LoginPage {...props} />
  }

  // 函数的组件默是没有组件名的,所以在组件树上显示的组件名为Anonymous
  // 在组件对象上有一个displayName可以修改组件的名称(是类不是实例)
  Auth.displayName = `${WrapCpn.name}Auth`

  return Auth
}

// 多个页面可能需要鉴权操作
const ProfileAuth = withAuth(ProfilePage)
const CartAuth = withAuth(CartPage)

export default class App extends PureComponent {
  render() {
    return (
      <Fragment>
        <ProfileAuth isLogin={true} />
        <CartAuth isLogin={false} />
        {/*
          =>
            ProfilePage
            LoginPage
        */}
      </Fragment>
    )
  }
}

劫持生命周期 --- 起到类似mixin的效果

import React, { PureComponent, Fragment } from 'react'

class Home extends PureComponent {
  render() {
    return <h2>Home</h2>
  }
}

class Page extends PureComponent {
  render() {
    return <h2>Page</h2>
  }
}

function calcRenderTime(WrapCpn) {
  return class Cpn extends PureComponent {
    constructor(props) {
      super(props)

      this.state = {
        beginTime: 0,
        endTime: 0
      }
    }

    UNSAFE_componentWillMount() {
      this.beginTime = Date.now()
    }

    componentDidMount() {
      this.endTime = Date.now()
      // ES中每一个类和函数都是一个只读的name属性,代表的是当前类或者函数的名称
      console.log(`${WrapCpn.name}的renderTime是${this.endTime - this.beginTime}ms`)
    }

    render() {
      return <WrapCpn />
    }

  }
}

const HomeTime = calcRenderTime(Home)
const PageTime = calcRenderTime(Page)

export default class App extends PureComponent {
  render() {
    return (
      <Fragment>
        <HomeTime />
        <PageTime />
      </Fragment>
    )
  }
}

在React中,如果出现组件使用的时候,应尽可能的使用组件组合的方式进行使用,而不要使用继承自自己组件的方式去进行使用

高阶函数的意义

其实早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:

  • Mixin 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的Mixin中的方法可能会相互冲突
  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

当然,HOC也有自己的一些缺陷:

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;

  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;

    • 例如props中已经存在一个属性叫nickName但是在扩展的时候,又扩展了一个nickName属性,导致后传入的属性将前面的属性给覆盖了

      从而导致了显示的数据和传入的数据的不一致性

      <Home name='Klaus' age={23} />
      
      export function enhanceProps(Cpn) {
        return props => <Cpn {...props} name="smo" region="china" />
      }
      
      // => name: smo
      //    age: 23
      //    ergion: china  
      

Hooks的出现,是开创性的,它解决了很多React之前的存在的问题

  • 比如this指向问题、比如hoc的嵌套复杂度问题等等;

三、ref的转发

在前面我们学习ref时讲过,ref不能应用于函数式组件:

  • 因为函数式组件没有实例,所以不能获取到对应的组件对象

但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?

  • 方式一:直接传入ref属性(错误的做法)
  • 方式二:通过forwardRef高阶函数;
import React, { PureComponent, createRef, forwardRef } from 'react'

// 使用forwaRef包裹后,其会在函数组件的第二个参数的位置传递一个ref对象,
// 其就是你调用的时候定义的ref的实例对象,此时将内部的ref指向这个传入的ref即可
// 使用ref来访问函数组件内部的对应元素
const Cpn = forwardRef(function(props, ref) {
  return <h3 ref={ref}>Cpn</h3>
})


export default class App extends PureComponent {
  constructor(props) {
    super(props)

    this.titleRef = createRef()
    this.cpnRef = createRef()
  }

  render() {
    return (
      <div>
        <h2 ref={this.titleRef}>title</h2>
        {/*
          默认情况下函数组件是没有实例对象和生命周期钩子的
          所以在函数组件上是不可以使用ref的

          tips: ref是由react内部进行管理的,其是不会作为props进行传递的
               但是我们可以借助forwardRef来帮助我们进行传递
        */}
        <Cpn ref={this.cpnRef} />
        <button onClick={e => this.printRef()} >print</button>
      </div>
    )
  }

  printRef() {
    console.log(this.titleRef.current)
    console.log(this.cpnRef.current)
  }
}

四、 Protals的简单使用

某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元 素上的)。

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的(例如Modal组件):

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:

  • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
  • 第二个参数(container)是一个 DOM 元素;

w7acRg.png

示例

需求: 我们准备开发一个Modal组件,它可以将它的子组件渲染到屏幕的中间位置

import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'

class Modal extends PureComponent {
  render() {
    // 可以通过this.props.children来获取到当前元素所有的子组件
    return ReactDOM.createPortal(
      this.props.children,
      document.getElementById('modal')
    )
  }
}


export default class App extends PureComponent {
  render() {
    return (
      <div>
         protal的基本使用
         <Modal>
          <h3>Modal组件</h3>
         </Modal>
      </div>
    )
  }
}

Protoal的本质就是帮助react-dom帮助我们去调用React.render方法,渲染到非父元素上

五、 Fragment

在之前的开发中,我们总是在一个组件中返回内容时包裹一个div元素:

w7rxwd.png

我们又希望可以不渲染这样一个div应该如何操作呢?

  • 使用Fragment
  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点
import React, { PureComponent, Fragment } from 'react'

export default class App extends PureComponent {
  render() {
    return (
      <Fragment>
        <h2>Klaus</h2>
        <h3>23</h3>
      </Fragment>
    )
  }
}

简化写法 (它看起来像空标签)

import React, { PureComponent, Fragment } from 'react'

export default class App extends PureComponent {
  render() {
    return (
      <>
        <h2>Klaus</h2>
        <h3>23</h3>
      </>
    )
  }
}

简化写法中 是不可以为其添加任何的属性的(例如key)

在循环中在Fragment上添加key属性是可以生效的,

但是不要在上添加样式,因为Fragment最后是不会渲染为一个真实的dom元素的

所以为其添加的样式也不会生效

六、 StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具。

  • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
  • 它为其后代元素触发额外的检查和警告, 但是不会影响代码的正常执行;
  • 严格模式检查仅在开发模式下运行;它们不会影响生产构建

StrictMode 本质上就是避免我们去使用一些过期的或者不建议使用的API或者语法的一种校验工具

可以为应用程序的任何部分启用严格模式:

  • 不会对 Header 和 Footer 组件运行严格模式检查;

  • 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查;

    w7sNkR.png

示例

import React, { PureComponent, StrictMode } from 'react'

class Home extends PureComponent {
  // 出现了警告
  UNSAFE_componentWillMount() {
    console.log('Home')
  }

  render() {
    return <h2>Home</h2>
  }
}

class Profile extends PureComponent {
  // 没有出现警告
  UNSAFE_componentWillMount() {
    console.log('Profile')
  }

  render(){
    return <h3>Profile</h3>
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <StrictMode>
          <Home />
        </StrictMode>
        <Profile />
      </div>
    )
  }
}

严格模式检查的是什么?

  1. 识别不安全的生命周期:

    • 例如componentWillMount
  2. 使用过时的ref API

    • 例如ref="title"
  3. 使用废弃的findDOMNode方法(了解)

    • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
    • 现在推荐使用ref
  4. 检查意外的副作用

    • 这个组件的某些生命周期方法(例如: constructor)会被调用两次;

    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用(bug或者性能损失);

    • 在生产环境中,是不会被调用两次的;

  5. 检测过时的context API (了解)

    • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的;

    上一篇 受控组件和非受控组件及ref 下一篇 React中样式的基本使用