简单实现React KeepAlive不依赖第三方库(附源码)

React KeepAlive 实现

大家好!打工人打工魂,我是阿祎,是一个前端开发经验3年+的菜菜子打工人👷🏻‍♂️,略微懂些后端和运维部署,React开发框架不支持KeepAlive组件,是用过Vue KeepAlive组件的银耳会呼吸的痛! 为了不让这个痛这么强烈,那么今天我就拉着大家的手简单(理解)实现一下React的KeepAlive组件。

KeepAlive组件实现特点Features

  • 使用React几个支持的api,Legacy除外,React18也可以放心使用。
  • 将缓存节点放在内存中,而不是挂载到body之外通过dispaly none隐藏方式,因此大幅减小对页面性能影响。

为什么要实现这个捏

首先实现某项功能都是有成本的,比如时间和精力投入,比如是否可能增加项目的复杂度,降低可维护性和稳定性的等等问题,需要在里面做一个平衡。对于很简单的管理系统而言,其实没必要过于复杂,简单高效的快速实现就行。 硬要实现这个的话,我觉得主要理由有如下。

  • 管理系统异常庞大,很多个菜单页面交互,很多表单页面交互,表单填写的时候需要去查看其他页面的内容,然后go back继续填写,这就涉及到之前表单的数据需要被保留下来,当再次切换到这个表单时之前未提交的填写的内容需要将其呈现,方便继续填写,作为开发也不想那么多页面一个个手动去(local/session storage 例如使用ahooks的useLocalStorageStateuseSessionStorageState)存取清除管理。
  • 很多页面需要保留之前请求的数据,避免在tab切换的时候多次重复请求,比如报表数据,本来就慢,切回来看还得等半天才加载出来,1s, 2s, 3s 嗯... 没啥体验感。(有人说了可以使用请求缓存,对可以在请求封装上边加上一层cache,或者直接使用Redux Toolkit的 RTK query, 感觉不是那么一劳永逸,(内心逼逼嗯,缓存有风险,后端都没有做,我还是不做吧))

RTK Query Overview | Redux Toolkit

So,不如实现(抄)一个吧。

核心原理实现

使用useRoutes Api 通过routes路由配置和location获得当前路由下面需要渲染的页面ReactElement节点。

jsx 复制代码
// Layout 组件内部
import {  useRoutes } from "react-router-dom"

// 匹配 当前路径要渲染的路由
const ele = useRoutes(routes, location)

CacheComponent组件实现

使用CacheComponent组件缓存children也就是上面的ele节点,再使用核心api createPortal(children, targetElement)挂载到组件内部的state上的div里面, 当tab切换和打开时,将其在父容器renderDiv上进行挂载和卸载。这里一个CacheComponent就相当于一个路由页面,通过active props 决定是否需要挂载或移除。里面蹲着的子页面蓄势待发就听active为true时,冲出去成为一个现眼包,闪亮登场。

jsx 复制代码
function CacheComponent({ active, children, name, renderDiv }: CacheComponentProps) {
    const [targetElement] = useState(() => document.createElement("div"))
    const activatedRef = useRef(false)
    activatedRef.current = activatedRef.current || active
    useEffect(() => {
        if (active) {
            // 挂载路由ReactElement(chidren)节点
            renderDiv.current?.appendChild(targetElement)
        } else {
            try {
                // 卸载路由ReactElement(chidren)节点
                renderDiv.current?.removeChild(targetElement)
            } catch (e) {
                console.log(e, "removeChild error")
            }
        }
    }, [active, renderDiv, targetElement])
    useEffect(() => {
        // 设置id 用于区分不同的路由ReactElement节点 获取激活状态 这里的id⭐️有大用 后面说
        targetElement.setAttribute("id", name)
    }, [name, targetElement])
    // 把当前的 chidren ReactElement 挂载到targetElement里面
    return <Fragment>{activatedRef.current && createPortal(children, targetElement)}</Fragment>
}

KeepAlive组件实现

KeepAlive组件接管CacheComponent的生死存亡,核心就在这里啦,我们把需要缓存的页面都放在cacheReactNodes里面, 展示子页面的父容器就在这里 上面的renderDiv就是 containerRef <div ref={containerRef} className="keep-alive" />

jsx 复制代码
const activeKey = useMemo(() => {
    return location.pathname + location.search
}, [location])

cacheReactNode的被操纵(拿捏🫴)的过程

jsx 复制代码
useLayoutEffect(() => {
    if (isNil(activeName)) {
        return
    }
    setCacheReactNodes(cacheReactNodes => {
        // maxLen最大缓存的页面数量 如果过大就把前面的页面缓存CacheComponent杀死
        if (length(cacheReactNodes) >= maxLen) {
            cacheReactNodes = slice(1, length(cacheReactNodes), cacheReactNodes)
        }
        // 找一下是不是已经存在里面蹲着了
        const cacheReactNode = cacheReactNodes.find(res => equals(res.name, activeName))
        if (isNil(cacheReactNode)) {
            // 没有就蹲进去吧,等待active为true的时候结束卧薪尝胆
            cacheReactNodes = append(
                {
                    name: activeName,
                    ele: children,
                },
                cacheReactNodes,
            )
        } else {
            // 已经在里面了 那就更新一下 children
            cacheReactNodes = map(res => {
                return equals(res.name, activeName) ? { ...res, ele: children } : res
            }, cacheReactNodes)
        }
        // 排除和包含
        return isNil(exclude) && isNil(include)
            ? cacheReactNodes
            : filter(({ name }) => {
                  if (exclude && includes(name, exclude)) {
                      return false
                  }
                  if (include) {
                      return includes(name, include)
                  }
                  return true
              }, cacheReactNodes)
    })
}, [children, activeName, exclude, maxLen, include])

给父亲管教的方法(给Layout方法)

当然我也要给它加一些手段,让它更灵活!在tab删除关闭时需要清除对应的缓存组件

jsx 复制代码
useImperativeHandle(
    aliveRef,
    () => ({
        // 获取缓存节点数组
        getCaches: () => cacheReactNodes,
        // 清除某一个缓存
        removeCache: (name: string) => {
            setCacheReactNodes(cacheReactNodes => {
                return filter(({ name: cacheName }) => !equals(cacheName, name), cacheReactNodes)
            })
        },
        // 毁灭吧,哟鸡喔挤哇卡咧
        clearCache: () => {
            setCacheReactNodes([])
        }
    }),
    [cacheReactNodes],
)

差不多都ok了,还差一个onActive,子页面激活的时候,子页面怎么知道我被激活了呢,也没有传props给它,也木有使用legacy api(cloneElement),这时候id⭐️有大用,就派上用场啦。id其实就是url路径对吧,那么子页面去获取包裹它的元素也就是那个targetElement Div身上的id值,然后对当前路由进行比较,就知道自己是否被激活了,那么封装一个hook就搞定!

useOnActive实现

jsx 复制代码
/**
 * @description 当路由激活时执行回调 KeepAlive
 * @param cb 回调函数
 * @param skipMount 是否跳过首次挂载
 */
export default function useOnActive(cb: () => any, skipMount = true) {
    const domRef = useRef<HTMLDivElement>(null)
    const location = useLocation()
    // 记录是否已经挂载
    const isMount = useRef(false)
    useEffect(() => {
        let destroyCb: any
        if (domRef.current) {
            const parent = domRef.current?.parentElement
            if (parent) {
                const id = parent.id
                const fullPath = location.pathname + location.search
                if (id === fullPath) {
                     // 是否跳过首次挂载
                    if (skipMount) {
                        if (isMount.current) destroyCb = cb()
                    } else {
                        destroyCb = cb()
                    }
                }
            }
        }
        isMount.current = true
        return () => {
            if (destroyCb && typeof destroyCb === "function") {
                destroyCb()
            }
        }
    }, [location])
    return domRef
}

useOnActive使用

jsx 复制代码
function Theme() {
    const domRef = useOnActive(() => {
        console.log("active Theme")
        return () => {
            console.log("clean Theme")
        }
    })
    return (
        <div ref={domRef}>
            <h1>Theme</h1>
            <Input placeholder="输入一个值 然后切换tab组件不会被销毁" />
        </div>
    )
}

Summary

由于篇幅有限。多tab实现很简单就不用细说啦,我把实现方案已经放在github代码库了,欢迎star和提issue。

github.com/irychen/sup...

完结散花

灵感来自于 github.com/liuye1296/r... 基于此优化加强

相关推荐
bysking21 分钟前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
58沈剑21 分钟前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构
王哲晓37 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_41140 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v41 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205871 小时前
web端手机录音
前端
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹1 小时前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js