如何在React中实现keep-alive?

写在开头

  • 不推荐你使用本文这个库,这个库会造成数据驱动断层(即你缓存后,切换回来,确实可以看到跟之前一样的dom,但是数据驱动此时失效了),下周我会写另外一个可以不断层的库解析

现代框架的本质其实还是 Dom 操作,今天看到一句话特别喜欢,不要给自己设限,到最后,大多数的技术本质是相同的。

  • Kafka , redis , sql事务写入 ,Nginx
    diff
    GRPC
    Pb 协议
    js
    Node.js
    

认真看完本文与源码,你会收获不少东西

框架谁优谁劣,就像 Web 技术的开发效率与 Native 开发的用户体验一样谁也不好一言而论谁高谁低,不过可以确定的是, web 技术已经越来越接近 Native 端体验了

  • 作者曾经是一位跨平台桌面端开发的前端工程师,由于是即时通讯应用,项目性能要求很高。于是苦寻名医,为了达到想要的性能,最终选定了非常冷门的几种优化方案拼凑在一起

  • 过程虽然非常曲折,但是市面上能用的方案都用到了,尝试过了,但是后面发现,极致的优化,并不是 1+1=2 ,要考虑业务的场景,因为一旦优化方案多了,他们之间的技术出发点,考虑的点可能会冲突。

  • 这也是前端需要架构师的原因,开发重型应用如果前端有了一位架构师,那么会少走很多弯路。

  • 后端也是如此

Vue.js 中的 keep-alive 使用:

Vue.js 中,尤大大是这样定义的:

keep-alive 主要用于保留组件状态或避免重新渲染

基础使用:

<keep-alive>
  <component :is="view"></component>
</keep-alive>

大概思路:

切换也是非常平滑,没有任何的闪屏(由于这里不支持gif图,可以看我的原文:https://segmentfault.com/a/1190000020413804)

特别提示:这里每个组件,下面还有一个1000行的列表哦~ 切换也是秒级

图看完了,开始梳理源码

第一步,初次渲染缓存

import {Provider , KeepAlive} from 'react-component-keepalive';

将需要缓存渲染的组件包裹,并且给一个 name 属性即可

例如:

import Content from './Content.jsx'

export default App extends React.PureComponent{
    render(){
        return(
            <div>
                <Provider>
                    <KeepAlive name="Content">
                        <Content/>
                    </KeepAlive>
                </Provider>
            </div>
        )
    }
}


这样这个组件你就可以在第二次需要渲染他的时候直接取缓存渲染了

下面是一组被缓存的一个组件,

仔细看上面的注释内容,再看当前 body 中多出来的 div

那么他们是不是对应上了呢?会是怎样缓存渲染的呢?

到底怎么缓存的

找到库的源码入口:

import Provider from './components/Provider';
import KeepAlive from './components/KeepAlive';
import bindLifecycle from './utils/bindLifecycle';
import useKeepAliveEffect from './utils/useKeepAliveEffect';

export {
  Provider,
  KeepAlive,
  bindLifecycle,
  useKeepAliveEffect,
};

最主要先看 Provider,KeepAlive 这两个组件:

缓存组件这个功能是通过 React.createPortal API 实现了这个效果。

react-component-keepalive 有两个主要的组件 <Provider><KeepAlive><Provider> 负责保存组件的缓存,并在处理之前通过 React.createPortal API 将缓存的组件渲染在应用程序的外面。缓存的组件必须放在 <KeepAlive> 中, <KeepAlive> 会把在应用程序外面渲染的组件挂载到真正需要显示的位置。

这样很明了了,原来如此

开始源码:

Provider 组件生命周期

public componentDidMount() {
    //创建`body`的div标签
    this.storeElement = createStoreElement();
    this.forceUpdate();
  }

createStoreElement 函数其实就是创建一个类似 UUID 的附带注释内容的 div 标签在 body

import {prefix} from './createUniqueIdentification';

export default function createStoreElement(): HTMLElement {
  const keepAliveDOM = document.createElement('div');
  keepAliveDOM.dataset.type = prefix;
  keepAliveDOM.style.display = 'none';
  document.body.appendChild(keepAliveDOM);
  return keepAliveDOM;
}

调用 createStoreElement 的结果:

然后调用 forceUpdate 强制更新一次组件

这个组件内部有大量变量锁:

export interface ICacheItem {
  children: React.ReactNode; //自元素节点
  keepAlive: boolean;   //是否缓存
  lifecycle: LIFECYCLE;   //枚举的生命周期名称
  renderElement?: HTMLElement;  //渲染的dom节点
  activated?: boolean;    //  已激活吗
  ifStillActivate?: boolean;      //是否一直保持激活
  reactivate?: () => void;     //重新激活的函数
}

export interface ICache {
  [key: string]: ICacheItem;
}

export interface IKeepAliveProviderImpl {
  storeElement: HTMLElement;   //刚才渲染在body中的div节点
  cache: ICache;  //缓存遵循接口 ICache  一个对象 key-value格式
  keys: string[]; //缓存队列是一个数组,里面每一个key是字符串,一个标识
  eventEmitter: any;  //这是自己写的自定义事件触发模块
  existed: boolean; //是否退出状态
  providerIdentification: string;  //提供的识别
  setCache: (identification: string, value: ICacheItem) => void; 。//设置缓存
  unactivate: (identification: string) => void; //设置不活跃状态
  isExisted: () => boolean; //是否退出,会返回当前组件的Existed的值
}

上面看不懂 别急,看下面:

接着是 Provider 组件真正渲染的内容代码:

<React.Fragment>
          {innerChildren}
          {
            keys.map(identification => {
              const currentCache = cache[identification];
              const {
                keepAlive,
                children,
                lifecycle,
              } = currentCache;
              let cacheChildren = children;
              
              //中间省略若干细节判断
              return ReactDOM.createPortal(
                (
                  cacheChildren
                    ? (
                      <React.Fragment>
                        <Comment>{identification}</Comment>
                        {cacheChildren}
                        <Comment
                          onLoaded={() => this.startMountingDOM(identification)}
                        >{identification}</Comment>
                      </React.Fragment>
                    )
                    : null
                ),
                storeElement,
              );
            })
          }
        </React.Fragment>


innerChildren 即是传入给 Providerchildren

一开始我们看见的缓存组件内容显示的都是一个注释内容 那为什么可以渲染出东西来呢

Comment 组件是重点

Comment 组件

public render() {
    return <div />;
  }

初始返回是一个空的 div 标签

但是看他的生命周期 ComponentDidmount

public componentDidMount() {
    const node = ReactDOM.findDOMNode(this) as Element;
    const commentNode = this.createComment();
    this.commentNode = commentNode;
    this.currentNode = node;
    this.parentNode = node.parentNode as Node;
    this.parentNode.replaceChild(commentNode, node);
    ReactDOM.unmountComponentAtNode(node);
    this.props.onLoaded();
  }


这个逻辑到这里并没有完,我们需要进一步查看 KeepAlive 组件源码

KeepAlive 源码:

组件 componentDidMount 生命周期钩子:

public componentDidMount() {
    const {
      _container,
    } = this.props;
    const {
      notNeedActivate,
      identification,
      eventEmitter,
      keepAlive,
    } = _container;
    notNeedActivate();
    const cb = () => {
      this.mount();
      this.listen();
      eventEmitter.off([identification, START_MOUNTING_DOM], cb);
    };
    eventEmitter.on([identification, START_MOUNTING_DOM], cb);
    if (keepAlive) {
      this.componentDidActivate();
    }
  }

  • 其他逻辑先不管,重点看

const cb = () => {
      this.mount();
      this.listen();
      eventEmitter.off([identification, START_MOUNTING_DOM], cb);
    };
    eventEmitter.on([identification, START_MOUNTING_DOM], cb);
    
    // 当接收到事件被触发后,调用`mout和listen`方法,然后取消监听这个事件
   
  private mount() {
    const {
      _container: {
        cache,
        identification,
        storeElement,
        setLifecycle,
      },
    } = this.props;
    this.setMounted(true);
    const {renderElement} = cache[identification];
    setLifecycle(LIFECYCLE.UPDATING);
    changePositionByComment(identification, renderElement, storeElement);
  }
  • changePositionByComment`这个函数是整个调用的重点,下面会解析

private listen() {
   const {
     _container: {
       identification,
       eventEmitter,
     },
   } = this.props;
   eventEmitter.on(
     [identification, COMMAND.CURRENT_UNMOUNT],
     this.bindUnmount = this.componentWillUnmount.bind(this),
   );
   eventEmitter.on(
     [identification, COMMAND.CURRENT_UNACTIVATE],
     this.bindUnactivate = this.componentWillUnactivate.bind(this),
   );
 }

listen 函数监听的自定义事件为了触发 componentWillUnmountcomponentWillUnactivate

COMMAND.CURRENT_UNMOUNT 这些都是枚举而已

  • changePositionByComment 函数:
export default function changePositionByComment(identification: string, presentParentNode: Node, originalParentNode: Node) {
  if (!presentParentNode || !originalParentNode) {
    return;
  }
  const elementNodes = findElementsBetweenComments(originalParentNode, identification);
  const commentNode = findComment(presentParentNode, identification);
  if (!elementNodes.length || !commentNode) {
    return;
  }
  elementNodes.push(elementNodes[elementNodes.length - 1].nextSibling as Node);
  elementNodes.unshift(elementNodes[0].previousSibling as Node);
  // Deleting comment elements when using commet components will result in component uninstallation errors
  for (let i = elementNodes.length - 1; i >= 0; i--) {
    presentParentNode.insertBefore(elementNodes[i], commentNode);
  }
  originalParentNode.appendChild(commentNode);
}

老规矩,上图解析源码:

  • 很多人看起来云里雾里,其实最终的实质就是通过了 Coment 组件的注释,来查找到对应的需要渲染真实节点再进行替换,而这些节点都是缓存在内存中, DOM 操作速度远比框架对比后渲染快。这里再次得到体现

这个库,无论是否路由组件都可以使用,[虚拟列表+缓存KeepAlive组件的Demo体验地址][1]

[库原链接地址][2]为了项目安全,我自己重建了仓库自己定制开发这个库

感谢原先作者的贡献 在我出现问题时候也第一时间给了我技术支持  谢谢!

新的库名叫 react-component-keepalive

直接可以在 npm 中找到

npm i react-component-keepalive

就可以正常使用了

如果你对 React 并不了解,可以看一些我之前的文章:

原创系列:

如何优化你的超大型React应用 【原创精读】

原创:从零实现一个简单版React (附源码)

最后

欢迎加我微信( CALASFxiaotan ),拉你进技术群,长期交流学习...

欢迎关注「 前端巅峰 」,认真学前端,做个有专业的技术人...

点个在看支持我吧,转发就更好了

我在看

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章