乐趣区

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

快来退出咱们吧!

“ 小和山的菜鸟们 ”,为前端开发者提供技术相干资讯以及系列根底文章。为更好的用户体验,请您移至咱们官网小和山的菜鸟们 (https://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>
    )
  }
}

渲染后果

咱们会发现多了一个 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>
    )
  }
}

渲染成果如下:

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 组件,并打印一条蕴含这些组件信息的正告音讯,如下所示:


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 版本中它依然无效,但在严格模式下,将显示以下正告:

下节预报

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

退出移动版