一 高阶组件源码级实践
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+
赞 👍