100 行代码实现 react keep alive 功能

react为什么不做keepalive,在Dan认为这是一种内存泄漏,不利于掌控

但是其实在真实的项目中还是有很多场景需求的。

keep alive 的适用场景

  1. 多Tabs后台,在一个Tabs下修改状态,然后跳到另外一个Tabs下面,然后在跳回去,状态不变
  2. 移动端经常有列表页面,点击详情之后跳到另一页面,返回之后还维持原来的分页和查询参数等信息

如何实现

看了很多实现,个人认为 这个实现方案 是比较优雅的,原作者git地址

不过这边方便理解和练习,实现了一个简易版,代码地址

一句话概括,如何实现keepalive,劫持每次访问的children,维护一个中间层缓存访问过的children,然后先把这个children通过createportal渲染到一个真实的dom节点上,但是这个节点并不挂到真实的父元素上,仅放置在内存中,当匹配规则命中之后,把内存中的dom元素挂到父dom元素上。

首先简单说下为什么切换路由不能缓存路由组件的状态,比如这里有一个根组件(渲染后生成一个div dom元素),三个路由组件(渲染后分别生成span1, span2, span3)

在路由切换时,react tree只会根据路由匹配到一个路由组件,然后生成对应的dom tree(其实中间还有fiber tree的构建,组件的状态其实是挂在fiber节点的hooks链表上的,这里就不解释了)

切换和渲染过程如下

实现 keep-alive 的核心,其实就是缓存(缓存的意思其实就是要挂在react tree上)访问过的组件,但是又不能把每个路由组件直接全部挂载到父元素上(挂上去也行,有的keep-alive方案就是挂上去 然后使用display:none隐藏,缓存的dom太多就会有严重的性能问题)。通常遇到想要缓存的问题,就会想到加一层中间层(我们将要实现的keep-alive就是一个中间层)

那么这个中间层的作用一句话概括就是:劫持 children 并将其缓存放入cache list,遍历cache list然后渲染,选择匹配到的组件,把这个组件对应的dom元素,挂载到父元素上。

注意这句话遍历cache list然后渲染,其实就是要在keep-alive的返回值中包含这些 cacheitme,否则在react的render阶段,这些组件就被卸载了。过程如下图,是访问router1,router2,router3,router2的示意图

注意在黄色的cache list下方分别是react router节点生成span1,然后把span1挂在cache1真实DOM元素上,最后又将cache1挂载到div上面。为什么要这么做,这么做就是为了不让真实的dom元素挂载很多的DOM节点,而是先将router对应的dom挂在自己缓存内部的一个dom上(这里使用了createProtal非常方便),根据激活状态决定要不要挂载父元素上。

代码实现

tsx 复制代码
import {
  ReactElement,
  ReactNode,
  RefObject,
  useLayoutEffect,
  useImperativeHandle,
  useRef,
  useState,
} from "react";
import { createPortal } from "react-dom";

export interface KeepAliveAPI {
  refresh: (key: string) => void;
  [key: string]: (key: string) => void;
}

export interface IKeepAlive {
  // 子元素
  children: ReactNode | ReactElement | null;
  // 当前激活的key
  activeKey: string;
  // 暴露给外部的ref
  keepRef: RefObject<KeepAliveAPI>;
}

export interface ICacheItem {
  // 劫持到的元素
  element: ReactNode | ReactElement | null;
  // 缓存的key值
  cacheKey: string;
  // 上次更新时间,可以为后续的lru做准备
  lastModified: number;
  // 渲染次数,真实渲染时,可以根据cacheKey + renderTime作为react渲染时的key,这样方便刷新内部状态
  renderTime: number;
}

export const KeepAlive: React.FC<IKeepAlive> = ({
  children,
  activeKey,
  keepRef,
}) => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const [cacheNodes, setCacheNodes] = useState<ICacheItem[]>([]);

  useImperativeHandle(keepRef, () => ({
    refresh: (key: string) => {
      const newCacheNodes = [...cacheNodes];
      const cacheIndex = newCacheNodes.findIndex(
        (cache) => cache.cacheKey === key
      );

      if (cacheIndex !== -1) {
        const cacheNode = newCacheNodes[cacheIndex];
        newCacheNodes[cacheIndex] = {
          ...cacheNode,
          lastModified: new Date().getTime(),
          renderTime: cacheNode.renderTime + 1,
        };
        setCacheNodes(newCacheNodes);
      }
    },
  }));

  useLayoutEffect(() => {
    const cacheNodeIndex = cacheNodes.findIndex(
      (cache) => cache.cacheKey === activeKey
    );
    if (cacheNodeIndex !== -1) {
      // 直接更新cacheNode的更新时间,
      setCacheNodes((pre) => {
        return [
          ...pre.slice(0, cacheNodeIndex),
          {
            ...cacheNodes[cacheNodeIndex],
            lastModified: new Date().getTime(),
            // 注意这里的element也要重新赋值,否则在children又props的情况下,一直放置的时历史props的reactnode,导致无法更新
            // 比如: <KeepAlive>
            //         <Test outCont={count} />
            //       </KeepAlive>
            // 不赋值children的话,Test组件的outCont不会更新
            element: children,
          },
          ...pre.slice(cacheNodeIndex + 1),
        ];
      });
    } else {
      setCacheNodes((pre) => [
        ...pre,
        {
          element: children,
          cacheKey: activeKey,
          lastModified: new Date().getTime(),
          renderTime: 1,
        },
      ]);
    }
  }, [activeKey, children]);

  return (
    <div ref={containerRef}>
      {containerRef.current
        ? cacheNodes.map((cache) => (
            <CacheElement
              key={cache.cacheKey}
              isActive={activeKey === cache.cacheKey}
              element={cache.element}
              renderKey={cache.cacheKey + cache.renderTime}
              containerRef={
                containerRef as React.MutableRefObject<HTMLDivElement>
              }
            />
          ))
        : null}
    </div>
  );
};

interface ICacheElementProps {
  isActive: boolean;
  element: ReactNode | ReactElement | null;
  containerRef: React.MutableRefObject<HTMLDivElement>;
  renderKey: string;
}
const CacheElement: React.FC<ICacheElementProps> = ({
  isActive,
  element,
  containerRef,
  renderKey,
}) => {
  const portalRef = useRef(document.createElement("div"));

  useLayoutEffect(() => {
    if (isActive) {
      containerRef.current.appendChild(portalRef.current);
    } else {
      containerRef.current.removeChild(portalRef.current);
    }
  }, [containerRef, isActive]);
  return createPortal(element, portalRef.current, renderKey);
};
相关推荐
TeleostNaCl2 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫3 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友3 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理5 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻5 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front6 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰6 小时前
纯flex布局来写瀑布流
前端·javascript·css
一袋米扛几楼987 小时前
【软件安全】什么是XSS(Cross-Site Scripting,跨站脚本)?
前端·安全·xss
向上的车轮7 小时前
Actix Web适合什么类型的Web应用?可以部署 Java 或 .NET 的应用程序?
java·前端·rust·.net
XiaoYu20028 小时前
第1章 核心竞争力和职业规划
前端·面试·程序员