高阶组件
认识高阶组件
什么是高阶组件呢? 相信很多朋友都用过高阶函数, 它们之间非常相似, 所以我们可以先来回顾一下什么是高阶函数
高阶函数的维基百科百科定义: 至少满足以下条件之一
- 接收一个或多个函数作为输入
- 输出一个函数
JavaScript 中比较常见的filter
、map
、reduce
都是高阶函数
那么什么是高阶组件呢?
- 高阶组件的英文是 Higher-Order Components, 简称 HOC
- 官方的定义: 高阶组件是参数为组件, 返回值为新组件的函数
我们可以进行如下解析
- 首先, 高阶组件本身不是一个组件, 而是一个函数
- 其次, 这个函数的参数及返回值都是一个组件
高阶组件的调用过程类似于下面的代码
1 | const EnhancedComponent = higherOrderComponent(WrapperComponent) |
高阶函数的编写过程类似于下面的代码
1 | function higherOrderComponent(WrapperComponent) { |
在 ES6 中, 类表达式中类名是可以省略的, 所以可以改写成下面的写法
1 | function higherOrderComponent(WrapperComponent) { |
另外, 组件的名称都可以通过displayName
来修改
1 | function higherOrderComponent(WrapperComponent) { |
完整代码
1 | import React, { PureComponent } from 'react' |
高阶组件并不是 React API 的一部分, 它是基于 React 的组合特性而形成的设计模式
高阶组件在一些 React 第三方库中非常常见
- 比如 redux 中的
connect
- 比如 react-router 中的
withRouter
高阶组件的使用
props 的增强
不修改原有代码的情况下, 添加新的props
1 | class Header extends PureComponent { |
我们可以通过一个高阶组件, 让使用者在不破坏原有结构的情况下对某个组件增强props
1 | function enhanceProps(WrapperCpn, otherProps) { |
利用高阶组件来共享 Context 属性
1 | import React, { PureComponent, createContext } from 'react' |
利用高阶组件withUser
1 | import React, { PureComponent, createContext } from 'react' |
渲染判断鉴权
在开发中, 我们可能遇到这样的场景
- 某些页面是必须用户登陆成功才能进行访问
- 如果用户没有登陆就访问, 那么直接跳转到登陆页面
这个时候, 我们就可以使用高阶组件来完成鉴权操作
1 | function LoginPage() { |
编写鉴权的高阶组件
1 | function loginAuth(Page) { |
完整代码
1 | import React, { PureComponent } from 'react' |
生命周期劫持
1 | import React, { PureComponent } from 'react' |
我们可以定义如下高阶组件
1 | function logRenderTime(WrapperCpn) { |
完整代码
1 | import React, { PureComponent } from 'react' |
高阶函数的意义
我们会发现利用高阶组件可以针对某些 React 代码进行更加优雅的处理
其实早期的 React 有提供组件之间的一种复用方式是 Mixin, 目前已经不再建议使用
- Mixin 可能会相互依赖, 相互耦合, 不利于代码维护
- 不同的 Mixin 中的方法可能会相互冲突
- Mixin 非常多时, 组件时可以感知到的, 甚至还要为其做相关处理, 这样会给代码造成滚雪球式的复杂性
当然, HOC 也有自己的一些缺陷
- HOC 需要在原组件上进行包裹或者嵌套, 如果大量使用HOC, 将会产生非常多的嵌套, 这让调试变得非常困难
- HOC 可以劫持
props
, 在不遵守约定的情况下也可能造成冲突
Hooks 的出现, 是开创性的, 它解决了很多 React 之前存在的问题, 比如this
指向问题、比如 HOC 的嵌套复杂度问题等等
组件补充
ref 转发
1 | import React, { PureComponent, createRef } from 'react' |
使用forwardRef
1 | import React, { PureComponent, createRef, forwardRef } from 'react' |
Portals
某些情况下, 我们希望渲染的内容独立于父组件, 甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到id
为root
的 DOM 元素上的)
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀方案
1 | ReactDOM.createPortal(child, container) |
- 第一个参数(
child
)是任何可渲染的 React 子元素, 例如一个元素, 字符串或fragment
- 第二个参数(
container
)是一个 DOM 元素
通常来讲, 当你从组件的render
方法返回一个元素时, 该元素将被挂载到 DOM 节点中离其最近的父节点
1 | render() { |
然而, 有时候将子元素插入到 DOM 节点中的不同位置也是有好处的
1 | render() { |
比如说, 我们准备开发一个Modal
组件, 它可以将它的子组件渲染到屏幕的中间位置
步骤一: 修改index.html
添加新的节点
1 | <div id="root"></div> |
步骤二: 编写这个节点的样式
1 | #modal { |
步骤三: 编写组件代码
1 | import React, { PureComponent } from 'react' |
Fragment
在之前的开发中, 我们总是在一个组件中返回内容时包裹一个div
元素
1 | export default class App extends PureComponent { |
我们会发现多了一个div
元素
- 这个
div
元素对于某些场景是需要的(比如我们就希望放到一个div
元素中, 在针对性设置样式) - 某些场景下这个
div
是没有必要的, 比如当前这里我可能希望所有的内容直接渲染到root
中即可
我们可以删除这个div
吗?
我们又希望可以不渲染这样一个div
应该如何操作呢?
- 使用
Fragment
Fragment
允许你将子列表分组, 而无需向 DOM 添加额外节点
1 | import React, { PureComponent, Fragment } from 'react' |
React 还提供了Fragment
的段语法
- 它看起来像空标签
<></>
1 | import React, { PureComponent, Fragment } from 'react' |
但是, 如果我们需要在Fragment
中添加key
, 那么就不能使用段语法
1 | { |
这里是不支持下面的写法的
1 | <key={item.name}> |
StrictMode
StrictMode
是一个用来突出显示应用程序中潜在问题的工具
- 与
Fragment
一样,StrictMode
不会渲染任何可见的 UI - 它为其后代元素触发额外的检查和警告
- 严格模式检查仅在开发模式下运行, 它们不会影响生产构建
可以为应用程序的任何部分启用严格模式
1 | import React from 'react' |
- 不会对
Header
和Footer
组件运行严格模式检查 - 但是,
ComponentOne
和ComponentTwo
以及它们所有的后代元素都将进行检查
到底检测什么东西呢?
- 识别不安全的生命周期
1 | import React, { PureComponent, StrictMode } from 'react' |
- 使用过时的 ref API
1 | import React, { PureComponent, StrictMode } from 'react' |
- 使用废弃的
findDOMNode
方法
在之前的 React API 中, 可以通过findDOMNode
来获取 DOM, 不过已经不再推荐使用了, 可以自行学习该 API
- 检查意外的副作用
1 | import React, { PureComponent, StrictMode } from 'react' |
Home
组件的constructor
会被调用两次- 这是严格模式下故意进行的操作, 让你来查看在这里写的一些逻辑代码被调用多次时, 是否会产生一些副作用
- 在生产环境中, 是不会被调用两次的
- 检测过时的 context API
早期的 Context 是通过static
属性声明 Context 对象属性的, 通过getChildContext
返回 Context 对象等方式来使用 Context
目前这种方式已经不再推荐使用, 可以自行学习该 API