一 高阶组件源码级实践
hoc的应用场景有很多,也有很多好的开源项目,供我们学习和参考,接下来我真对三个方向上的功能用途,分别从源码角度解析HOC的用途。
1 强化prop- withRoute
用过withRoute的同学,都明白其用途,withRoute用途就是,对于没有被Route包裹的组件,给添加history对象等和路由相关的状态,方便我们在任意组件中,都能够获取路由状态,进行路由跳转,这个HOC目的很清楚,就是强化props,把Router相关的状态都混入到props中,我们看看具体怎么实现的。
function withRouter(Component) {
const displayName = `withRouter(${
Component.displayName || Component.name})`;
const C = props => {
/* 获取 */
const {
wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{
context => {
return (
<Component
{
...remainingProps}
{
...context}
ref={
wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
C.displayName = displayName;
C.WrappedComponent = Component;
/* 继承静态属性 */
return hoistStatics(C, Component);
}
export default withRouter
withRoute的流程实际很简单,就是先从props分离出ref和props,然后从存放整个route对象上下文RouterContext取出route对象,然后混入到原始组件的props中,最后用hoistStatics继承静态属性。至于hoistStatics我们稍后会讲到。
2 控制渲染案例 connect
由于connect源码比较长和难以理解,所以我们提取精髓,精简精简再精简, 总结的核心功能如下,connect的作用也有合并props,但是更重要的是接受state,来控制更新组件。下面这个代码中,为了方便大家理解,我都给简化了。希望大家能够理解hoc如何派发和控制更新流的。
import store from './redux/store'
import {
ReactReduxContext } from './Context'
import {
useContext } from 'react'
function connect(mapStateToProps){
/* 第一层: 接收订阅state函数 */
return function wrapWithConnect (WrappedComponent){
/* 第二层:接收原始组件 */
function ConnectFunction(props){
const [ , forceUpdate ] = useState(0)
const {
reactReduxForwardedRef ,...wrapperProps } = props
/* 取出Context */
const {
store } = useContext(ReactReduxContext)
/* 强化props:合并 store state 和 props */
const trueComponentProps = useMemo(()=>{
/* 只有props或者订阅的state变化,才返回合并后的props */
return selectorFactory(mapStateToProps(store.getState()),wrapperProps)
},[ store , wrapperProps ])
/* 只有 trueComponentProps 改变时候,更新组件。 */
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent
{
...trueComponentProps}
ref={
reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, WrappedComponent, trueComponentProps]
)
useEffect(()=>{
/* 订阅更新 */
const checkUpdate = () => forceUpdate(new Date().getTime())
store.subscribe( checkUpdate )
},[ store ])
return renderedWrappedComponent
}
/* React.memo 包裹 */
const Connect = React.memo(ConnectFunction)
/* 处理hoc,获取ref问题 */
if(forwardRef){
const forwarded = React.forwardRef(function forwardConnectRef( props,ref) {
return <Connect {
...props} reactReduxForwardedRef={
ref} reactReduxForwardedRef={
ref} />
})
return hoistStatics(forwarded, WrappedComponent)
}
/* 继承静态属性 */
return hoistStatics(Connect,WrappedComponent)
}
}
export default Index
connect 涉及到的功能点还真不少呢,首先第一层接受订阅函数,第二层接收原始组件,然后用forwardRef处理ref,用hoistStatics 处理静态属性的继承,在包装组件内部,合并props,useMemo缓存原始组件,只有合并后的props发生变化,才更新组件,然后在useEffect内部通过store.subscribe()订阅更新。这里省略了Subscription概念,真正的connect中有一个Subscription专门负责订阅消息。
3 赋能组件-缓存生命周期 keepaliveLifeCycle
之前笔者写了一个react缓存页面的开源库react-keepalive-router,可以实现vue中 keepalive + router功能,最初的版本没有缓存周期的,但是后来热心读者,期望在被缓存的路由组件中加入缓存周期,类似activated这种的,后来经过我的分析打算用HOC来实现此功能。
于是乎react-keepalive-router加入了全新的页面组件生命周期 actived 和 unActived, actived 作为缓存路由组件激活时候用,初始化的时候会默认执行一次 , unActived 作为路由组件缓存完成后调用。但是生命周期需要用一个 HOC 组件keepaliveLifeCycle 包裹。
使用
import React from 'react'
import {
keepaliveLifeCycle } from 'react-keepalive-router'
@keepaliveLifeCycle
class index extends React.Component<any,any>{
state={
activedNumber:0,
unActivedNumber:0
}
actived(){
this.setState({
activedNumber:this.state.activedNumber + 1
})
}
unActived(){
this.setState({
unActivedNumber:this.state.unActivedNumber + 1
})
}
render(){
const {
activedNumber , unActivedNumber } = this.state
return <div style={
{
marginTop :'50px' }} >
<div> 页面 actived 次数: {
activedNumber} </div>
<div> 页面 unActived 次数:{
unActivedNumber} </div>
</div>
}
}
export default index
效果:

原理
import {
lifeCycles} from '../core/keeper'
import hoistNonReactStatic from 'hoist-non-react-statics'
function keepaliveLifeCycle(Component) {
class Hoc extends React.Component {
cur = null
handerLifeCycle = type => {
if (!this.cur) return
const lifeCycleFunc = this.cur[type]
isFuntion(lifeCycleFunc) && lifeCycleFunc.call(this.cur)
}
componentDidMount() {
const {
cacheId} = this.props
cacheId && (lifeCycles[cacheId] = this.handerLifeCycle)
}
componentWillUnmount() {
const {
cacheId} = this.props
delete lifeCycles[cacheId]
}
render=() => <Component {
...this.props} ref={
cur => (this.cur = cur)}/>
}
return hoistNonReactStatic(Hoc,Component)
}
keepaliveLifeCycle 的原理很简单,就是通过ref或获取 class 组件的实例,在 hoc 初始化时候进行生命周期的绑定, 在 hoc 销毁阶段,对生命周期进行解绑, 然后交给keeper统一调度,keeper通过调用实例下面的生命周期函数,来实现缓存生命周期功能的。
二 高阶组件的注意事项
1 谨慎修改原型链
function HOC (Component){
const proDidMount = Component.prototype.componentDidMount
Component.prototype.componentDidMount = function(){
console.log('劫持生命周期:componentDidMount')
proDidMount.call(this)
}
return Component
}
这样做会产生一些不良后果。比如如果你再用另一个同样会修改 componentDidMount 的 HOC 增强它,那么前面的 HOC 就会失效!同时,这个 HOC 也无法应用于没有生命周期的函数组件。
2 继承静态属性
在用属性代理的方式编写HOC的时候,要注意的是就是,静态属性丢失的问题,前面提到了,如果不做处理,静态方法就会全部丢失。
手动继承
我们可以手动将原始组件的静态方法copy到 hoc组件上来,但前提是必须准确知道应该拷贝哪些方法。
function HOC(Component) {
class WrappedComponent extends React.Component {
/*...*/
}
// 必须准确知道应该拷贝哪些方法
WrappedComponent.staticMethod = Component.staticMethod
return WrappedComponent
}
引入第三方库
这样每个静态方法都绑定会很累,尤其对于开源的hoc,对原生组件的静态方法是未知的,我们可以使用 hoist-non-react-statics 自动拷贝所有的静态方法:
import hoistNonReactStatic from 'hoist-non-react-statics'
function HOC(Component) {
class WrappedComponent extends React.Component {
/*...*/
}
hoistNonReactStatic(WrappedComponent,Component)
return WrappedComponent
}
3 跨层级捕获ref
高阶组件的约定是将所有 props 传递给被包装组件,但这对于 refs 并不适用。那是因为 ref 实际上并不是一个 prop - 就像 key 一样,它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。我们可以通过forwardRef来解决这个问题。
/**
*
* @param {*} Component 原始组件
* @param {*} isRef 是否开启ref模式
*/
function HOC(Component,isRef){
class Wrap extends React.Component{
render(){
const {
forwardedRef ,...otherprops } = this.props
return <Component ref={
forwardedRef} {
...otherprops} />
}
}
if(isRef){
return React.forwardRef((props,ref)=> <Wrap forwardedRef={
ref} {
...props} /> )
}
return Wrap
}
class Index extends React.Component{
componentDidMount(){
console.log(666)
}
render(){
return <div>hello,world</div>
}
}
const HocIndex = HOC(Index,true)
export default ()=>{
const node = useRef(null)
useEffect(()=>{
/* 就可以跨层级,捕获到 Index 组件的实例了 */
console.log(node.current.componentDidMount)
},[])
return <div><HocIndex ref={
node} /></div>
}
打印结果:

如上就解决了,HOC跨层级捕获ref的问题。
4 render中不要声明HOC
🙅错误写法:
class Index extends React.Component{
render(){
const WrapHome = HOC(Home)
return <WrapHome />
}
}
如果这么写,会造成一个极大的问题,因为每一次HOC都会返回一个新的WrapHome,react diff会判定两次不是同一个组件,那么每次Index 组件 render触发,WrapHome,会重新挂载,状态会全都丢失。如果想要动态绑定HOC,请参考如下方式。
🙆正确写法:
const WrapHome = HOC(Home)
class index extends React.Component{
render(){
return <WrapHome />
}
}
三 总结
本文从高阶组件功能为切入点,介绍二种不同的高阶组件如何编写,应用场景,以及实践。涵盖了大部分耳熟能详的开源高阶组件的应用场景,如果你觉得这篇文章对你有启发,最好还是按照文章中的demo,跟着敲一遍,加深印象,知道什么场景用高阶组件,怎么用高阶组件。
实践是检验真理的唯一标准,希望大家能把高阶组件码起来,用起来。
最后 , 送人玫瑰,手留余香,觉得有收获的朋友可以给笔者点赞,关注一波 ,陆续更新前端超硬核文章。
回顾往期react经典好文
react进阶系列
- 「react进阶」年终送给react开发者的八条优化建议
840+赞👍
react源码系列
react-hooks系列
玩转react-hooks,自定义hooks设计模式及其实战
160+👍赞react-hooks如何使用
90+赞👍
开源项目系列
- 「react缓存页面」从需求到开源(我是怎么样让产品小姐姐刮目相看的)
250+赞 👍