乐趣区

关于前端:一些关于react的keepalive功能相关知识在这里上

一些对于 react 的 keep-alive 性能相干常识在这里(上)

下一篇讲这类插件的 ”大坑“, 如果你想全面理解的话肯定要读下一篇哦。

背景

     这是在 2022 年开发中 PM 提的一个需要, 某个 table 被用户输出了一些搜搜条件并且浏览到了第 3 页, 那如果我跳转到其余路由后返回以后页面, 心愿搜寻条件还在, 并且仍处于第三页, 这不就是 vue 外面的 keep-alive 标签吗, 但我以后的我的项目是用 react 编写的。

     此次讲述了我经验了 “ 应用内部插件 ”-> “ 放弃内部插件 ”-> “ 学习并自研插件 ”-> “ 了解了相干插件的窘境 ” -> “ 期待 react18Offscreen😓”, 所以论断是举荐急躁期待 react18 的自反对, 然而学习以后相似插件的原理对本人还是很有启发的。

     一个库不能只说本人的长处也要把毛病展现进去, 否则会给使用者代码隐患, 但我浏览了很多网上的文章与官网, 大多都没有讲出相干的原理的细节, 并且没有人对以后存在的 bug 进行剖析, 我这里会对相干奇怪的问题进行具体的解说, 我上面展现代码是参考了网上的几种计划后稍作改进的。

一、插件调研

     咱们一起看一下市场上当初有哪些 ’ 成熟 ’ 的计划。

     第一个: react-keep-alive : 官网很正规, 851 Star, 用法上也与 vue 的 keep-alive 很靠近, 然而差评太多了, 以及 3 年没更新了, 并且很多网上的文章也都说这个库很坑, 一起看看它的评论吧 (抬走下一位)。

     第二个: react-router-cache-route : 这个库是针对路由级别的一个缓存, 无奈对组件级别失效, 引入后要替换掉以后的路由组件库, 危险不小并且缓存的量级太大了 (抬走下一位)。

     第三个: react-activation : 这个库是网上大家比拟认可的库, issues 也比拟少并且不 ’ 致命 ’, 并且能够反对组件级别的缓存(其实它做不到, 还是有 bug), 我尝试着应用到本人团队的我的项目里后成果还能够, 然而因为此插件没有大团队反对并且外部全是中文, 最初也没有进行应用。

     通过上述调研, 让我对 react-activation 的原理产生了趣味, 遂想在团队外部开发一款相似的插件不就能够了吗, 对 keep-alive 的探索从此揭开序幕。

二、外围原理、

     先赘述一下前提, react 的虚构 dom 构造是一棵树, 这棵树的某个节点被移除会导致所有子节点也被销毁 所以写代码时才须要用 Memo 进行包裹。(记住这张图)

     比方我想缓存 "B2 组件" 的状态, 那其实要做的就是让 "B 组件" 被销毁时 "B2 组件不被销毁", 从图上可知当 "B 组件" 被销毁时 "A 组件" 是不会被销毁的, 因为 "A 组件" 不在 "B 组件" 的上级, 所以咱们要做的就是让 "A 组件" 来生成 "B2 组件", 再把"B2" 组件插入到"B 组件外部"

     所谓的在 ”A 组件 ” 下渲染, 就是在 "A 组件" 外面:

function A(){
  return (
    <div>
      <B1></B1>
    </div>
  )
}

     再应用 appendChilddiv 外面的 dom 元素全副转移到 "B 组件" 外面即可。

三、appendChild 后 react 仍然失常执行

     尽管应用 appendChild"A 组件"外面的 dom 元素插入到 "B 组件", 然而react 外部的各种渲染曾经实现, 比方咱们在 "B1 组件" 内应用 useState 定义了一个变量叫 'n' , 当 'n' 变动时触发的 dom 变动也都曾经被 react 记录, 所以不会影响每次进行dom diff 后的元素操作。

     并且在 "A 组件" 上面也能够应用 "Consumer" 接管到 "A 组件" 内部的 "Provider", 但也引出一个问题, 就是如果不是 "A 组件" 外的 "Provider" 无奈被接管到, 上面是 react-actication 的解决形式:

     其实这样侵入 react 源代码逻辑的操作还是要谨慎, 咱们也能够用粗鄙一点的形式略微代替一下, 次要利用 Provider 能够反复写的个性, 将 Provider 与其 value 传入进去实现 context 的失常, 然而这样也显然是不敌对的。

     所以 react-activation 官网才会注明上面这段话:

四、插件的架构设计介绍

     先看用法:

const RootComponent: React.FC = () => (
        <KeepAliveProvider>
            <Router>
                <Routes>
                    <Route path={'/'} element={<Keeper cacheId={'home'}> <Home/> </Keeper>
                        }
                    />
                </Routes>
            </Router>
        </KeepAliveProvider>
  )

     咱们应用 KeepAliveProvider 组件来贮存须要被缓存的组件的相干信息, 并且用来渲染被缓存的组件, 也就是充当 ”A 组件 ” 的角色。

     KeepAliveProvider组件外部应用 Keeper 组件来标记组件应该渲染在哪里? 也就是要用 Keeper"B1 组件"+"B2 组件" 包裹起来, 这样咱们就晓得初始化好的组件该放到哪里。

     cacheId也就是缓存的 id, 每个id 对应一个组件的缓存信息, 后续会用来监控每个缓存的组件是否被 ” 激活 ”, 以及清理组件缓存。

五、KeepAliveProvider 开发

     这里先列出一个 ” 概念代码 ”, 因为间接看残缺的代码会晕掉。

import CacheContext from './cacheContext'
const KeepAliveProvider: React.FC = (props) => {const [catheStates, dispatch]: any = useReducer(cacheReducer, {})
     const mount = useCallback(({ cacheId, reactElement}) => {if (!catheStates || !catheStates[cacheId]) {
                dispatch({
                    type: cacheTypes.CREATE,
                    payload: {
                        cacheId,
                        reactElement
                    }
                })
            }
        },
        [catheStates]
    )
 return (<CacheContext.Provider value={{ catheStates, mount}}>
            {props.children}
            {Object.values(catheStates).map((item: any) => {const { cacheId = '', reactElement} = item
                const cacheState = catheStates[`${cacheId}`];
                const handleDivDom = (divDom: Element) => {const doms = Array.from(divDom.childNodes)
                        if (doms?.length) {
                            dispatch({
                                type: cacheTypes.CREATED,
                                payload: {
                                    cacheId,
                                    doms
                                }
                            })
                        }
                }
                return (
                    <div 
                     key={`${cacheId}`} 
                     id={`cache- 外层渲染 -${cacheId}`} 
                     ref={(divDom) => divDom && handleDivDom(divDom)}>
                        {reactElement}
                    </div>
        </CacheContext.Provider>
    )
}

export default KeepAliveProvider

代码解说

1. catheStates 存储所有的缓存信息

     它的数据格式如下:

{
  cacheId: 缓存 id,
  reactElement: 真正要渲染的内容,
  status: 状态,
  doms?: dom 元素,
 }
2. mount 用来初始化组件

     将组件状态变为 'CREATE', 并且将要渲染的组件储存起来, 就是上图外面"B1 组件",

    const mount = useCallback(({cacheId, reactElement}) => {if (!catheStates || !catheStates[cacheId]) {
                dispatch({
                    type: cacheTypes.CREATE,
                    payload: {
                        cacheId,
                        reactElement}
                })
            }
        },
        [catheStates]
    )
3. CacheContext 传递与贮存信息

     CacheContext 是咱们专门创立用来贮存数据的, 他会向各个 Keeper 散发各种办法。

import React from "react";
let CacheContext = React.createContext()
export default CacheContext;
4. {props.children} 渲染 KeepAliveProvider 标签中的内容
5. div 渲染须要缓存的组件

     这里放一个 div 作为渲染组件的容器, 当咱们能够获取到这个 div 的实例时则对其 childNodes 贮存到catheStates, 然而这里有个问题, 这种写法只能解决同步渲染的子组件, 如果组件异步渲染则无奈贮存正确的childNodes

6. 异步渲染的组件

     假如有如下这种异步的组件, 则无奈获取到正确的 dom 节点, 所以如果 domchildNodes为空, 咱们须要监听 dom 的状态, 当 dom 内被插入元素时执行。

 function HomePage() {const [show, setShow] = useState(false)
    useEffect(() => {setShow(true)
    }, [])
    return show ? <div>home</div>: null;
 }

     将 handleDivDom 办法的代码做一些批改:

let initDom = false
const handleDivDom = (divDom: Element) => {handleDOMCreated()
    !initDom && divDom.addEventListener('DOMNodeInserted', handleDOMCreated)
    function handleDOMCreated() {if (!cacheState?.doms) {const doms = Array.from(divDom.childNodes)
            if (doms?.length) {
                initDom = true
                dispatch({
                    type: cacheTypes.CREATED,
                    payload: {
                        cacheId,
                        doms
                    }
                })
            }
        }
    }
}

     当没有获取到 childNodes 则为 div 增加 "DOMNodeInserted"事件, 来监测是否有 dom 插入到了 div 外部。

     所以总结来说, 上述代码就是负责了初始化相干数据, 并且负责渲染组件, 然而具体渲染什么组件还须要咱们应用 Keeper 组件。

六、编写渲染占位的 Keeper

     在应用插件的时候, 咱们理论须要被缓存的组件都是写在 Keeper 组件里的, 就像上面这种写法:

<Keeper cacheId="home">
  <Home />
  <User />
  <footer>footer</footer>
</Keeper>

     此时咱们并不要真的在 Keeper 组件外面来渲染组件, 把 props.children 储存起来, 在 Keeper 外面放一个 div 来占位, 并且当检测到有数据中有须要被缓存的 dom 时, 则应用 appendChilddom 放到本人的外部。

import React, {useContext, useEffect} from 'react'
import CacheContext from './cacheContext'

export default function Keeper(props: any) {const { cacheId} = props
    const divRef = React.useRef(null)
    const {catheStates, dispatch, mount} = useContext(CacheContext)
    useEffect(() => {const catheState = catheStates[cacheId]
        if (catheState && catheState.doms) {
            const doms = catheState.doms
            doms.forEach((dom: any) => {(divRef?.current as any)?.appendChild?.dom
            })
        } else {
            mount({
                cacheId,
                reactElement: props.children
            })
        }
    }, [catheStates])
    return <div id={`keeper- 原始地位 -${cacheId}`} ref={divRef}></div>
}

     这里会多出一个 div, 我也没发现太好的方法, 我尝试应用doms 把这个 div 元素替换掉, 这就会导致没有 react 的数据驱动了, 也尝试将这个 dom 设置 "hidden = true" 而后将doms 插入到这个 div 的兄弟节点, 但最初也没胜利。

七、Portals 属性介绍

     看到网上有些插件没有应用 appendChild 而是应用 react 提供的 来实现的, 感觉挺好玩的就在这里也聊一下。

     Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优良的计划, 直白说就是能够指定我要把 child 渲染到哪个 dom 元素中, 用法如下:

ReactDOM.createPortal(child, "指标 dom")

react 官网是这样形容的: 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 款式时,但你须要子组件可能在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

     因为这里须要指定在哪里渲染 child, 所以大须要有明确的 child 属性与指标dom, 然而咱们这个插件可能更适宜异步操作, 也就是咱们只是将数据放在 catheStates 外面, 须要取的时候来取, 而不是渲染时就要明确指定的模式来设计。

八、监控缓存被激活

     咱们要实时监控到底哪个组件被 ” 激活 ”, “ 激活 ” 的定义是组件被初始化后被缓存起来, 之后的每次应用缓存都叫 ” 激活 ”, 并且每次组件被激活调用 activeCache 办法来通知用户以后哪个组件被 ” 激活 ” 了。

     为什么要通知用户哪个组件被激活了? 大家能够想想这样一个场景, 用户点击了 table 的第三条数据的编辑按钮跳转到编辑页面, 编辑后返回列表页, 此时可能须要咱们更新一下列表里第三条的状态, 此时就须要晓得哪些组件被激活了。

     还有一种状况如下图所示, 这是一种鼠标悬停会呈现 tip 提醒语, 如果此时点击按钮产生跳转页面会导致, 当你返回列表页面时这个 tip 居然还在 ….

     当然我指的不是 element-ui, 是咱们本人的ui 库, 过后看了一下起因, 是因为这个组件只有检测到鼠标来到某些元素才会让 tip 隐没, 然而跳页了并且以后页面的所有 domkeep-alive被缓存下来了, 导致了这个 tip 没有被清理。

     它的代码如下:

`    useEffect(() => {const catheState = catheStates[cacheId]
        if (catheState && catheState.doms) {console.log('激活了:', cacheId)
            activeCache(cacheId)
        }
    }, [])

     之所以 useEffect 的参数只传了个空数组, 因为每次组件被 ” 激活 ” 都能够执行, 因为每次 Keeper 组件每次会被销毁的, 所以这里能够执行。

最终应用演示

     在组件中应用来检测指定的组件是否被更新, 第一个参数是要监测的 id, 也就是Keeper 身上的cacheId, 第二个参数是callback

     用户应用插件时, 能够在本人的组件内按上面的写法来进行监控:

    useEffect(() => {const cb = () => {console.log('home 被激活了')
        }
        cacheWatch(['home'], cb)
        return () => {removeCacheWatch(['home'], cb)
        }
    }, [])
具体实现

     在 KeepAliveProvider 中定义 activeCache 办法:

     每次激活组件, 就去数组内寻找监听办法进行执行。

const [activeCacheObj, setActiveCacheObj] = useState<any>({})
    const activeCache = useCallback((cacheId) => {if (activeCacheObj[cacheId]) {activeCacheObj[cacheId].forEach((fn: any) => {fn(cacheId)
                })
            }
        },
        [catheStates, activeCacheObj]
    )

     增加一个检测办法:

     每次都把 callback 放到对应的对象身上。

    const cacheWatch = useCallback((ids: string[], fn) => {ids.forEach((id: string) => {if (activeCacheObj[id]) {activeCacheObj[id].push(fn)
            } else {activeCacheObj[id] = [fn]
            }
        })
        setActiveCacheObj({...activeCacheObj})
      },
     [activeCacheObj]
    )

     还要有一个移除监控的办法:

    const removeCacheWatch = (ids: string[], fn: any) => {ids.forEach((id: string) => {if (activeCacheObj[id]) {const index = activeCacheObj[id].indexOf(fn)
                activeCacheObj.splice(index, 1)
            }
        })
        setActiveCacheObj({...activeCacheObj})
    }

     删除缓存的办法, 须要在 cacheReducer 外面减少删除办法, 留神这里须要每个 remove 所有 dom, 而不是仅对 cacheStates 的数据进行删除。


case cacheTypes.DESTROY:
    if (cacheStates[payload.cacheId]) {const doms = cacheStates?.[payload.cacheId]?.doms
        if (doms) {doms.forEach((element) => {element.remove()
            })
        }
    }
    delete cacheStates[payload.cacheId]
    return {...cacheStates}

end

     下一篇讲这类插件的 ”大坑“, 如果你想全面理解的话肯定要读下一篇哦, 这次就是这样, 心愿与你一起提高。

退出移动版