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

快来加入咱们吧!

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

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

前言

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

本文会向你介绍如下内容:react

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

高阶组件

认识高阶组件

什么是高阶组件呢?相信不少同窗都据说过,也用过高阶函数,它们很是类似,因此咱们能够先来回顾一下什么是高阶函数git

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

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

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

那么什么是高阶组件?算法

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

由此,我么能够分析出:设计模式

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

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

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 的加强

一、不修改原有代码的状况下,添加新的 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 的警告

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

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

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

image.png


二、关于使用过期字符串 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


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

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


四、检测意外的反作用

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

    console.log('home constructor')
  }

  UNSAFE_componentWillMount() {}

  render() {
    return <h2 ref="home">Home</h2>
  }
}
复制代码

五、检测过期的 context API

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

img.png

下节预告

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

相关文章
相关标签/搜索