快来加入我们吧!
"小和山的菜鸟们",为前端开发者提供技术相关资讯以及系列基础文章。为更好的用户体验,请您移至我们官网小和山的菜鸟们 ( xhs-rookies.com/ ) 进行学习,及时获取最新文章。
"Code tailor" ,如果您对我们文章感兴趣、或是想提一些建议,微信关注 “小和山的菜鸟们” 公众号,与我们取的联系,您也可以在微信上观看我们的文章。每一个建议或是赞同都是对我们极大的鼓励!
前言
这节我们将介绍 React
中高阶组件,以及高阶组件到底有什么用,以及对高阶组件的补充。
本文会向你介绍以下内容:
- 认识高阶组件
- 高阶组件的使用
- 高阶组件的意义
- 高阶组件的注意点
- 高阶组件中转发
refs
Portals
Fragment
- 严格模式-
StrictMode
高阶组件
认识高阶组件
什么是高阶组件呢?相信很多同学都听说过,也用过高阶函数,它们非常相似,所以我们可以先来回顾一下什么是高阶函数。
高阶函数的维基百科定义:至少满足以下条件之一:
- 接受一个或多个函数作为输入;
- 输出一个函数;
JavaScript
中比较常见的 filter
、map
、reduce
都是高阶函数。
那么什么是高阶组件?
- 高阶组件的英文是 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
}
所以,在我们的开发中,高阶组件可以帮助我们做哪些事情呢?往下看吧!
高阶组件的使用
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
React
的 diff
算法(称为协调)使用组件标识来确定它是应该更新现有子树还是将其丢弃并挂载新子树。 如果从 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
接受一个渲染函数,其接收 props
和 ref
参数并返回一个 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 为 root
的 DOM
元素上的)。
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>
</>
)
}
}
严格模式-StrictMode
StrictMode
是一个用来突出显示应用程序中潜在问题的工具,与 Fragment
一样,StrictMode
不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。
你可以为应用程序的任何部分启用严格模式。例如:
import React from 'react'
function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
)
}
在上述的示例中,不会对 Header
和 Footer
组件运行严格模式检查。但是,ComponentOne
和 ComponentTwo
以及它们的所有后代元素都将进行检查。
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
,敬请期待!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!