React 系列十一:高阶组件以及组件补充

510 阅读14分钟

快来加入我们吧!

"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 ( xhs-rookies.com/ ) 进行学习,及时获取最新文章。

"Code tailor" ,如果您对我们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与我们取的联系,您也可以在微信上观看我们的文章。每一个建议或是赞同都是对我们极大的鼓励!

前言

这节我们将介绍 React 中高阶组件,以及高阶组件到底有什么用,以及对高阶组件的补充。

本文会向你介绍以下内容:

  • 认识高阶组件
  • 高阶组件的使用
  • 高阶组件的意义
  • 高阶组件的注意点
  • 高阶组件中转发 refs
  • Portals
  • Fragment
  • 严格模式-StrictMode

高阶组件

认识高阶组件

什么是高阶组件呢?相信很多同学都听说过,也用过高阶函数,它们非常相似,所以我们可以先来回顾一下什么是高阶函数

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

  • 接受一个或多个函数作为输入;
  • 输出一个函数;

JavaScript 中比较常见的 filtermapreduce 都是高阶函数。

那么什么是高阶组件?

  • 高阶组件的英文是 Higher-Order Components,简称为 HOC,是 React 中用于复用组件逻辑的一种高级技巧。
  • 官方的定义:高阶组件是参数为组件,返回值为新组件的函数

由此,我么可以分析出:

  • 高阶组件本身不是一个组件,而是一个函数
  • 这个函数的参数是一个组件,返回值也是一个组件

高阶组件的调用过程类似于这样:

const EnhancedComponent = higherOrderComponent(WrappedComponent)

组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。

高阶函数的编写过程类似于这样:

  • 返回类组件,适合有状态处理、用到生命周期的需求
function higherOrderComponent(WrapperComponent) {
  return class NewComponent extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
}
  • 返回函数组件,适合简单的逻辑处理
function higherOrderComponent(WrapperComponent) {
  return (props) => {
    if (props.token) {
      return <WrapperComponent />
    } else {
      return <></>
    }
  }
}

在 ES6 中,类表达式中类名是可以省略的,所以有以下这种写法:

function higherOrderComponent(WrapperComponent) {
  return class extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
}

组件名称是可以通过 displayName 来修改的:

function higherOrderComponent(WrapperComponent) {
  class NewComponent extends PureComponent {
    render() {
      return <WrapperComponent />
    }
  }
  NewComponent.displayName = 'xhsRookies'
  return NewComponent
}

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

所以,在我们的开发中,高阶组件可以帮助我们做哪些事情呢?往下看吧!

高阶组件的使用

props 的增强

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

假如我们有如下案例:

class XhsRookies extends PureComponent {
  render() {
    const { name, age } = this.props
    return <h2>XhsRookies {name + age}</h2>
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <XhsRookies name="xhsRookies" age={18} />
      </div>
    )
  }
}

我们可以通过一个高阶组件,在不破坏原有 props 的情况下,对组件增强,假如需要为 XhsRookies 组件的 props 增加一个 height 属性,我们可以这样做:

class XhsRookies extends PureComponent {
  render() {
    const { name, age } = this.props
    return <h2>XhsRookies {name + age}</h2>
  }
}

function enhanceProps(WrapperComponent, newProps) {
  return (props) => <WrapperComponent {...props} {...newProps} />
}

const EnhanceHeader = enhanceProps(XhsRookies, { height: 1.88 })

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <EnhanceHeader name="xhsRookies" age={18} />
      </div>
    )
  }
}

利用高阶组件来共享 Context

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

const UserContext = createContext({
  nickname: '默认',
  level: -1,
})

function XhsRookies(props) {
  return (
    <UserContext.Consumer>
      {(value) => {
        const { nickname, level } = value
        return <h2>Header {'昵称:' + nickname + '等级' + level}</h2>
      }}
    </UserContext.Consumer>
  )
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <UserContext.Provider value={{ nickname: 'xhsRookies', level: 99 }}>
          <XhsRookies />
        </UserContext.Provider>
      </div>
    )
  }
}

我们定义一个高阶组件 ShareContextHOC,来共享 context

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

const UserContext = createContext({
  nickname: '默认',
  level: -1,
})

function ShareContextHOC(WrapperCpn) {
  return (props) => {
    return (
      <UserContext.Consumer>
        {(value) => {
          return <WrapperCpn {...props} {...value} />
        }}
      </UserContext.Consumer>
    )
  }
}

function XhsRookies(props) {
  const { nickname, level } = props
  return <h2>Header {'昵称:' + nickname + '等级:' + level}</h2>
}

function Footer(props) {
  const { nickname, level } = props
  return <h2>Footer {'昵称:' + nickname + '等级:' + level}</h2>
}

const NewXhsRookies = ShareContextHOC(Header)

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <UserContext.Provider value={{ nickname: 'xhsRookies', level: 99 }}>
          <NewXhsRookies />
        </UserContext.Provider>
      </div>
    )
  }
}

渲染判断鉴权

在开发中,我们会遇到以下场景:

  • 某些页面是必须用户登录成功才能进入
  • 如果用户没有登录成功,直接跳转到登录页面

这种场景下我们可以使用高阶组件来完成鉴权操作:

function LoginPage() {
  // 登录页面
  return <h2>LoginPage</h2>
}

function HomePage() {
  // 登录成功可访问页面
  return <h2>HomePage</h2>
}

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

使用鉴权组件:

import React, { PureComponent } from 'react'

function loginAuthority(Page) {
  return (props) => {
    if (props.isLogin) {
      // 如果登录成功 返回成功页面
      return <Page />
    } else {
      // 如果为登录成功 返回登录页面
      return <LoginPage />
    }
  }
}

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

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

const AuthorityPassPage = loginAuthority(HomePage)

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <AuthorityPassPage isLogin={true} />
      </div>
    )
  }
}

生命周期劫持

当多个组件,需要在生命周期中做一些事情,而这些事情都是相同的逻辑,我们就可以利用高阶组件,统一帮助这些组件,完成这些工作,如下例子:

import React, { PureComponent } from 'react'

class Home extends PureComponent {
  componentDidMount() {
    const nowTime = Date.now()
    console.log(`Home渲染使用时间:${nowTime}`)
  }

  render() {
    return (
      <div>
        <h2>Home</h2>
        <p>我是home的元素,哈哈哈</p>
      </div>
    )
  }
}

class Detail extends PureComponent {
  componentDidMount() {
    const nowTime = Date.now()
    console.log(`Detail渲染使用时间:${nowTime}`)
  }

  render() {
    return (
      <div>
        <h2>Detail</h2>
        <p>我是detail的元素,哈哈哈</p>
      </div>
    )
  }
}

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

我们可以利用高阶租价,帮助完成 home 组件和 detail 组件的 componentDidMount 生命周期函数:

import React, { PureComponent } from 'react'

function logRenderTime(WrapperCpn) {
  return class extends PureComponent {
    componentDidMount() {
      const nowTime = Date.now()
      console.log(`${WrapperCpn.name}渲染使用时间:${nowTime}`)
    }

    render() {
      return <WrapperCpn {...this.props} />
    }
  }
}

class Home extends PureComponent {
  render() {
    return (
      <div>
        <h2>Home</h2>
        <p>我是home的元素,哈哈哈</p>
      </div>
    )
  }
}

class Detail extends PureComponent {
  render() {
    return (
      <div>
        <h2>Detail</h2>
        <p>我是detail的元素,哈哈哈</p>
      </div>
    )
  }
}

const LogHome = logRenderTime(Home)
const LogDetail = logRenderTime(Detail)

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <LogHome />
        <LogDetail />
      </div>
    )
  }
}

高阶组件的意义

通过上面不同情况对高阶组件的使用,我们可以发现利用高阶组件可以针对某些 React 代码进行更加优雅的处理。

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

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

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

  • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
  • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;

合理利用高阶组件,会对我们开发有很大的帮助。

高阶组件的注意点

不要在 render 方法中使用 HOC

Reactdiff 算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 render 返回的组件与前一个渲染中的组件相同(===),则 React 通过将子树与新子树进行区分来递归更新子树。 如果它们不相等,则完全卸载前一个子树。

通常,你不需要考虑这点。但对 HOC 来说这一点很重要,因为这代表着你不应在组件的 render 方法中对一个组件应用 HOC

render() {
  // 每次调用 render 函数都会创建一个新的 EnhancedComponent
  // EnhancedComponent1 !== EnhancedComponent2
  const EnhancedComponent = enhance(MyComponent);
  // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
  return <EnhancedComponent />;
}

这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。

如果在组件之外创建 HOC,这样一来组件只会创建一次。因此,每次 render 时都会是同一个组件。一般来说,这跟你的预期表现是一致的。

const EnhancedComponent = enhance(MyComponent)

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

在极少数情况下,你需要动态调用 HOC。你可以在组件的生命周期方法或其构造函数中进行调用。

refs 不会被传递

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

组件的补充

高阶组件中转发 refs

前面我们提到了在高阶组件中,refs 不会被传递,但我们在开发中有可能会遇到需要在高阶组件中转发 refs,那么我们该怎么解决呢?幸运的是,我们可以使用React.forwardRef API 来帮助解决这个问题。

让我们从一个输出组件 props 到控制台的 HOC 示例开始:

function logProps(WrappedComponent) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps)
      console.log('new props:', this.props)
    }

    render() {
      return <WrappedComponent {...this.props} />
    }
  }

  return LogProps
}

logProps HOC 透穿所有 props 到其包裹的组件,所以渲染结果将是相同的。例如:我们可以使用该 HOC 记录所有传递到 “fancy button” 组件的 props

class FancyButton extends React.Component {
  focus() {
    // ...
  }

  // ...
}

// 我们导出 LogProps,而不是 FancyButton。
// 虽然它也会渲染一个 FancyButton。
export default logProps(FancyButton)

到此前,这个示例正如前面所说,refs 将不会透传下去。如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

import FancyButton from './FancyButton'

const ref = React.createRef()

// 我们导入的 FancyButton 组件是高阶组件(HOC)LogProps。
// 尽管渲染结果将是一样的,
// 但我们的 ref 将指向 LogProps 而不是内部的 FancyButton 组件!
// 这意味着我们不能调用例如 ref.current.focus() 这样的方法
;<FancyButton label="Click Me" handleClick={handleClick} ref={ref} />

这个时候,我们就可以利用 React.forwardRef API 明确的将 refs 转发到内部的 FancyButton 组件。React.forwardRef 接受一个渲染函数,其接收 propsref 参数并返回一个 React 节点。

function logProps(Component) {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props:', prevProps)
      console.log('new props:', this.props)
    }

    render() {
      const { forwardedRef, ...rest } = this.props

      // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
      return <Component ref={forwardedRef} {...rest} />
    }
  }

  // 注意 React.forwardRef 回调的第二个参数 “ref”。
  // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  return React.forwardRef((props, ref) => {
    return <LogProps {...props} forwardedRef={ref} />
  })
}

这样我们就可以在高阶组件中传递 refs 了。

Portals

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

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

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

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

render() {
  // React 挂载了一个新的 div,并且把子元素渲染其中
  return (
    <div>
      {this.props.children}
    </div>
  );
}

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

render() {
  // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  return ReactDOM.createPortal(
    this.props.children,
    domNode
  );
}

比如说,我们准备开发一个 TabBar 组件,它可以将它的子组件渲染到屏幕顶部位置:

  • 第一步:修改 index.html 添加新的节点
<div id="root"></div>
<!-- 新节点 -->
<div id="TabBar"></div>
  • 第二步:编写这个节点的样式
#TabBar {
  position: fixed;
  width: 100%;
  height: 44px;
  background-color: red;
}
  • 第三步:编写组件代码
import React, { PureComponent } from 'react'
import ReactDOM from 'react-dom'

class TabBar extends PureComponent {
  constructor(props) {
    super(props)
  }

  render() {
    return ReactDOM.createPortal(this.props.children, document.getElementById('TabBar'))
  }
}

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <TabBar>
          <button>按钮1</button>
          <button>按钮2</button>
          <button>按钮3</button>
          <button>按钮4</button>
        </TabBar>
      </div>
    )
  }
}

Fragment

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

export default class App extends PureComponent {
  render() {
    return (
      <div>
        <h2>微信公众号:小和山的菜鸟们</h2>
        <button>点赞</button>
        <button>关注</button>
      </div>
    )
  }
}

渲染结果

7FB293B8-6095-44E9-B80E-1A2D1B3B90AF.png

我们会发现多了一个 div 元素:

  • 这个 div 元素对于某些场景是需要的(比如我们就希望放到一个 div 元素中,再针对性设置样式)
  • 某些场景下这个 div 是没有必要的,比如当前这里我可能希望所有的内容直接渲染到 root 中即可;

当我们删除这个 div 时,会报错,如果我们希望不渲染这个 div 应该如何操作?

  • 使用 Fragment
  • Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
export default class App extends PureComponent {
  render() {
    return (
      <Fragment>
        <h2>微信公众号:小和山的菜鸟们</h2>
        <button>点赞</button>
        <button>关注</button>
      </Fragment>
    )
  }
}

渲染效果如下:

image.png

React 还提供了 Fragment

它看起来像空标签 <></>

export default class App extends PureComponent {
  render() {
    return (
      <>
        <h2>微信公众号:小和山的菜鸟们</h2>
        <button>点赞</button>
        <button>关注</button>
      </>
    )
  }
}

**注意:**如果我们需要在 Fragment 中添加属性,比如 key,我们就不能使用段语法了

严格模式-StrictMode

StrictMode 是一个用来突出显示应用程序中潜在问题的工具,与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

**注意:**严格模式检查仅在开发模式下运行;它们不会影响生产构建。

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

import React from 'react'

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  )
}

在上述的示例中,会对 HeaderFooter 组件运行严格模式检查。但是,ComponentOneComponentTwo 以及它们的所有后代元素都将进行检查。

StrictMode 目前有助于:

  • 识别不安全的生命周期
  • 关于使用废弃的 findDOMNode 方法的警告
  • 检测意外的副作用
  • 检测过时的 context API
  • 关于使用过时字符串 ref API 的警告

1、识别不安全的生命周期

某些过时的生命周期方法在异步 React 应用程序中使用是不安全的。但是,如果你的应用程序使用了第三方库,很难确保它们不使用这些生命周期方法。

当启用严格模式时,React 会列出使用了不安全生命周期方法的所有 class 组件,并打印一条包含这些组件信息的警告消息,如下所示:

image.png


2、关于使用过时字符串 ref API 的警告

以前,React 提供了两种方法管理 refs 的方式:

  • 已过时的字符串 ref API 的形式
  • 回调函数 API 的形式。

尽管字符串 ref API 在两者中使用更方便,但是它有一些缺点,因此官方推荐采用回调的方式

React 16.3 新增了第三种选择,它提供了使用字符串 ref 的便利性,并且不存在任何缺点:

class MyComponent extends React.Component {
  constructor(props) {
    super(props)

    this.inputRef = React.createRef()
  }

  render() {
    return <input type="text" ref={this.inputRef} />
  }

  componentDidMount() {
    this.inputRef.current.focus()
  }
}

由于对象 ref 主要是为了替换字符串 ref 而添加的,因此严格模式现在会警告使用字符串 ref


3、关于使用废弃的 findDOMNode 方法的警告

React 支持用 findDOMNode 来在给定 class 实例的情况下在树中搜索 DOM 节点。通常你不需要这样做,因为你可以将 ref 直接绑定到 DOM 节点,由于此方法已经废弃,这里就不展开细讲了,如感兴趣,可自行学习。


4、检测意外的副作用

  • 这个组件的 constructor 会被调用两次;
  • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用;
  • 在生产环境中,是不会被调用两次的;
class Home extends PureComponent {
  constructor(props) {
    super(props)

    console.log('home constructor')
  }

  UNSAFE_componentWillMount() {}

  render() {
    return <h2 ref="home">Home</h2>
  }
}

5、检测过时的 context API

早期的 Context 是通过 static 属性声明 Context 对象属性,通过 getChildContext 返回 Context 对象等方式来使用 Context 的;不过目前这种方法已经过时,过时的 context API 容易出错,将在未来的主要版本中删除。在所有 16.x 版本中它仍然有效,但在严格模式下,将显示以下警告:

img.png

下节预告

本节我们学习了 React 中高阶组件以及组件补充的内容,在下一个章节我们将开启新的学习 React-Router ,敬请期待!