小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
在JS中只要满足以下两个条件中的一个,就可以被称之为高阶函数:
- 接受一个或多个函数作为输入
- 输出一个函数
JavaScript中比较常见的filter、map、reduce都是高阶函数
而React中同样存在高阶组件的概念:
高阶组件的英文是 Higher-Order Components,简称为 HOC
官方的定义: 高阶组件是参数为组件,返回值为新组件的函数
- 高阶组件 本身不是一个组件,而是一个函数
- 这个函数的参数是一个组件,返回值也是一个组件;
高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式
const EnhancedComponent = higherOrderComponent(WrappedComponent)
一个简单的HOC
import { PureComponent } from 'react'
class App extends PureComponent {
render() {
return (
<div>
App: { this.props.name }
</div>
)
}
}
// 参数是一个组件,所以参数名推荐首字母大写
function HOC(Wrapped) {
// 在React DevTool中组件的名称默认是组件的class后的名称
// 如果是类表达式,那么组件的名称就是类父组件的名称
const NewCpn = class extends PureComponent {
render() {
// HOC只是一个包裹的组件,实际使用props的Wrapped组件
// 所以需要进行参数的传递
return <Wrapped {...this.props}/>
}
}
// 每一个类组件都有一个displayName
// 可以显示的设置类组件在React DevTool中显示的名称
NewCpn.displayName = 'HOC'
return NewCpn
}
export default HOC(App)
< App name="App" />
// HOC要求返回值和参数都是一个组件
// 所以HOC返回值和参数接收的组件即可以是类组件,也可以是函数组件
function HOC(Wrapped) {
const NewCpn = function(props) {
return <Wrapped {...props} />
}
NewCpn.displayName = 'HOC'
return NewCpn
}
案例
劫持props
使用props共享相同的属性
Cpn
import { PureComponent } from 'react'
class Cpn extends PureComponent {
render() {
return (
<ul>
<li>nickname: {this.props?.nickname}</li>
<li>level: {this.props?.level}</li>
</ul>
)
}
}
function enhanceLevel(Wrapped) {
return function(props) {
// 在原本组件基础上扩充level属性
return <Wrapped {...props} level={66} />
}
}
export default enhanceLevel(Cpn)
App
import { PureComponent } from 'react'
import Cpn from './Cpn'
export default class App extends PureComponent {
render() {
return (
<div>
<Cpn nickname="coderwxf"/>
</div>
)
}
}
使用context共享相同的属性
Cpn
import { PureComponent, createContext } from 'react'
export const userContext = createContext({
nickname: 'anonyamous',
level: -1
})
class Cpn extends PureComponent {
render() {
return (
<ul>
<li>nickname: {this.props?.nickname}</li>
<li>level: {this.props?.level}</li>
</ul>
)
}
}
// 参考withRouter,HOC方法一般使用with开头 + 大驼峰命名
function withUser(Wrapped) {
return function(props) {
return (
<userContext.Consumer>
{
user => {
return <Wrapped {...props} {...user} />
}
}
</userContext.Consumer>
)
}
}
export default withUser(Cpn)
App
import { PureComponent } from 'react'
import Cpn, {userContext} from './Cpn'
export default class App extends PureComponent {
render() {
return (
<div>
<userContext.Provider value={{nickname: 'Klaus', level: 6}}>
<Cpn />
</userContext.Provider>
{/* 使用context的默认值 */}
<Cpn />
</div>
)
}
}
劫持JSX
Cpn
import { PureComponent } from 'react'
class Login extends PureComponent {
render() {
return <h2>Login</h2>
}
}
// define HOC
function withAuth(WrappedComponent) {
// 这里之所以定义一个具名函数,是为了在ReactDevTool工具中可以很方便的查找该组件
// 即给该组件一个具体的名称
return function Auth(props) {
const { isLogin } = props
return isLogin ? <WrappedComponent /> : <Login />
}
}
class Cpn extends PureComponent {
render() {
return (
<div>
Cpn
</div>
)
}
}
export default withAuth(Cpn)
App
import { PureComponent } from 'react'
import Cpn from './Cpn'
export default class App extends PureComponent {
render() {
return (
<Cpn isLogin={false} />
)
}
}
劫持生命周期
Cpn.js
import { PureComponent } from 'react'
class Cpn extends PureComponent {
render() {
return (
<div>
Cpn
</div>
)
}
}
function withRenderTime(Wrapped) {
return class HOC extends PureComponent {
render() {
this.startTime = Date.now()
return <Wrapped {...this.props} />
}
componentDidMount() {
// 无论类组件还是函数组件,本质上都是构造函数
// 所以组件上有一个name属性,表示当前组件的名称
console.log(`${Wrapped.name}渲染时间为${Date.now() - this.startTime}ms`)
}
}
}
export default withRenderTime(Cpn)
App
import { PureComponent } from 'react'
import Cpn from './Cpn'
export default class App extends PureComponent {
render() {
return (
<Cpn />
)
}
}
HOC的意义
HOC最大的作用就是可以对传入的组件进行劫持,也就是可以将多个组件所共同的逻辑抽离到HOC中进行实现 或者 可以通过HOC对组件进行功能扩展
早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
- Mixin 可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
当然,HOC也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难;
- HOC可以劫持props,在不遵守约定的情况下也可能造成冲突;
Cpn
import { PureComponent, createContext } from 'react'
export const userContext = createContext({
nickname: 'anonyamous',
level: -1
})
class Cpn extends PureComponent {
render() {
return (
<ul>
<li>nickname: {this.props?.nickname}</li>
<li>level: {this.props?.level}</li>
</ul>
)
}
}
function withUser(Wrapped) {
return function(props) {
return (
<userContext.Consumer>
{
user => {
// 此时如果user和props中有prop的名称是同名的时候
// 就会发生冲突,后一个会将前一个给覆盖
return <Wrapped {...user} {...props} />
}
}
</userContext.Consumer>
)
}
}
export default withUser(Cpn)
App
import { PureComponent } from 'react'
import Cpn, {userContext} from './Cpn'
export default class App extends PureComponent {
render() {
return (
<div>
<userContext.Provider value={{nickname: 'Klaus', level: 6}}>
<Cpn nickname="coderwxf"/>
</userContext.Provider>
</div>
)
}
}
Ref转发
ref不能应用于函数式组件,因为函数式组件没有实例,所以不能获取到对应的组件对象
为此React为我们提供了一个HOC,即为forwardRef, 可以帮助我们获取React的函数组件
import { PureComponent, forwardRef, createRef } from 'react'
// 使用forwardRef这个HOC进行包裹后,函数组件会多传入一个参数,即为ref
// 也就是在实际使用HOC返回的组件时候传入的ref
// 我们可以通过这个ref参数来获取函数组件中对应元素所对应的DOM对象
const FunCpn = forwardRef(function(props, ref) {
return <p ref={ref}>funCpn</p>
})
export default class Cpn extends PureComponent {
constructor(props) {
super(props)
this.funCpnRef = createRef()
}
render() {
return (
<div>
{/*
默认情况下,ref属性是不可以作为组件的props进行传递的
因为ref属性是一个特殊的属性,会交给react内部进行管理
*/}
<FunCpn ref={ this.funCpnRef }/>
<button onClick={() => this.handleClick()}>获取函数组件对应的DOM对象</button>
</div>
)
}
handleClick() {
console.log(this.funCpnRef.current)
}
}