定义
-
参数 / 返回值:组件
-
纯函数
-
不会修改传入的组件
-
不会使用继承来复制其行为
-
没有副作用
-
使用HOC的原因
相同功能组件复用
权限控制:条件渲染,控制组件的渲染逻辑(渲染劫持)
组件渲染性能追踪、日志打点:劫持被处理组件的生命周期
实现方式
属性代理
使用组合的方式,将组件包装在容器上,依赖父子组件的生命周期关系
-
返回stateless的函数组件
-
返回class组件
操作props
// 可以通过属性代理,拦截父组件传递过来的porps并进行处理。
// 返回一个无状态的函数组件
function HOC(WrappedComponent) {
const newProps = { type: 'HOC' }
return (props) => <WrappedComponent {...props} {...newProps} />
}
// 返回一个有状态的 class 组件
function HOC(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = { type: 'HOC' }
return <WrappedComponent {...this.props} {...newProps} />
}
}
}
抽象state
// 通过属性代理无法直接操作原组件的state,可以通过props和cb抽象state
function HOC(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
this.onChange = this.onChange.bind(this);
}
onChange = (event) => {
this.setState({
name: event.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onChange,
},
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
// 使用
@HOC
class Example extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
通过props实现条件渲染
// 通过props来控制是否渲染及传入数据
import * as React from 'react'
function HOC(WrappedComponent) {
return (props) => <div>{props.isShow ? <WrappedComponent {...props} /> : <div>暂无数据</div>}</div>
}
export default HOC
其他元素wrapper传入的组件
function withBackgroundColor(WrappedComponent) {
return class extends React.Component {
render() {
return (
<div style={{ backgroundColor: '#ccc' }}>
<WrappedComponent {...this.props} {...newProps} />
</div>
)
}
}
}
反向继承
- 使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的 render() 方法中返回 super.render() 方法
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render()
}
}
}
-
允许HOC通过this访问到原组件,可以直接读取和操作原组件的state/ref等;
-
可以通过super.render()获取传入组件的render,可以有选择的渲染劫持;
-
劫持原组件生命周期方法
function HOC(WrappedComponent) {
const didMount = WrappedComponent.prototype.componentDidMount
// 继承了传入组件
return class HOC extends WrappedComponent {
async componentDidMount() {
// 劫持 WrappedComponent 组件的生命周期
if (didMount) {
await didMount.apply(this)
}
// ...
}
render() {
//使用 super 调用传入组件的 render 方法
return super.render()
}
}
}
- 读取/操作原组件的state
function HOC(WrappedComponent) {
const didMount = WrappedComponent.prototype.componentDidMount
// 继承了传入组件
return class HOC extends WrappedComponent {
async componentDidMount() {
if (didMount) {
await didMount.apply(this)
}
// 将 state 中的 number 值修改成 2
this.setState({ number: 2 })
}
render() {
//使用 super 调用传入组件的 render 方法
return super.render()
}
}
}
- 条件渲染
const HOC = (WrappedComponent) =>
class extends WrappedComponent {
render() {
if (this.props.isRender) {
return super.render()
} else {
return <div>暂无数据</div>
}
}
}
- 修改react树
// 修改返回render结果
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
const tree = super.render()
const newProps = {}
if (tree && tree.type === 'input') {
newProps.value = 'something here'
}
const props = {
...tree.props,
...newProps,
}
const newTree = React.cloneElement(tree, props, tree.props.children)
return newTree
}
}
}
属性代理和反向继承对比
-
属性代理:从“组合”角度出发,有利于从外部操作wrappedComp,可以操作props,或者在wrappedComp 外加一些拦截器(如条件渲染等)
-
反向继承:从“继承”角度出发,从内部操作wrappedComp,可以操作组件内部的state,生命周期和render等,功能能加强大
页面复用(属性代理)
views/PageA.js
import React from 'react'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MovieList'
class PageA extends React.Component {
state = {
movieList: [],
}
/* ... */
async componentDidMount() {
const movieList = await fetchMovieListByType('comedy')
this.setState({
movieList,
})
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无喜剧" />
}
}
export default PageA
views/PageB.js
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MovieList'
class PageB extends React.Component {
state = {
movieList: [],
}
// ...
async componentDidMount() {
const movieList = await fetchMovieListByType('action')
this.setState({
movieList,
})
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无动作片" />
}
}
export default PageB
HOC
import React from 'react'
import React from 'react'
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
return class extends React.Component {
async componentDidMount() {
const data = await fetchingMethod()
this.setState({
data,
})
}
render() {
return <WrappedComponent data={this.state.data} {...defaultProps} {...this.props} />
}
}
}
使用
views/PageA.js
import React from 'react'
import withFetchingHOC from '../hoc/withFetchingHOC'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MovieList'
const defaultProps = { emptyTips: '暂无喜剧' }
export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps)
views/PageB.js
import React from 'react'
import withFetchingHOC from '../hoc/withFetchingHOC'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MovieList'
const defaultProps = { emptyTips: '暂无动作片' }
export default withFetchingHOC(MovieList, fetchMovieListByType('action'), defaultProps)
views/PageOthers.js
import React from 'react'
import withFetchingHOC from '../hoc/withFetchingHOC'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MovieList'
const defaultProps = {...}
export default withFetchingHOC(MovieList, fetchMovieListByType('some-other-type'), defaultProps)
更符合 里氏代换原则(Liskov Substitution Principle LSP),任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
权限控制(属性代理)
import React from 'react'
import { whiteListAuth } from '../lib/utils' // 鉴权方法
function AuthWrapper(WrappedComponent) {
return class AuthWrappedComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
permissionDenied: -1,
}
}
async componentDidMount() {
try {
await whiteListAuth() // 请求鉴权接口
this.setState({
permissionDenied: 0,
})
} catch (err) {
this.setState({
permissionDenied: 1,
})
}
}
render() {
if (this.state.permissionDenied === -1) {
return null // 鉴权接口请求未完成
}
if (this.state.permissionDenied) {
return <div>功能即将上线,敬请期待~</div>
}
return <WrappedComponent {...this.props} />
}
}
}
export default AuthWrapper
组件渲染性能(反向继承)
- 如何计算一个组件render期间的渲染耗时
import React from 'react'
// Home 组件
class Home extends React.Component {
render() {
return <h1>Hello World.</h1>
}
}
// HOC
function withTiming(WrappedComponent) {
let start, end
return class extends WrappedComponent {
constructor(props) {
super(props)
start = 0
end = 0
}
componentWillMount() {
if (super.componentWillMount) {
super.componentWillMount()
}
start = +Date.now()
}
componentDidMount() {
if (super.componentDidMount) {
super.componentDidMount()
}
end = +Date.now()
console.error(`${WrappedComponent.name} 组件渲染时间为 ${end - start} ms`)
}
render() {
return super.render()
}
}
}
export default withTiming(Home)