1500行TypeScript代碼在React中實現組件keep-alive

42

clipboard.png

現代框架的本質其實還是Dom操作,今天看到一句話特别喜歡,不要給自己設限,到最後,大多數的技術本質是相同的。

例如後端用到的Kafka , redis , sql事務寫入 ,Nginx負載均衡算法,diff算法,GRPC,Pb 協議的序列化和反序列化,鎖等等,都可以在前端被類似的大量複用邏輯,即便jsNode.js都是單線程的

認真看完本文與源碼,你會收獲不少東西

clipboard.png

框架誰優誰劣,就像Web技術的開發效率與Native開發的用戶體驗一樣誰也不好一言而論誰高誰低,不過可以确定的是,web技術已經越來越接近Native端體驗了

作者是一位跨平台桌面端開發的前端工程師,由于是即時通訊應用,項目性能要求很高。于是苦尋名醫,為了達到想要的性能,最終選定了非常冷門的幾種優化方案拼湊在一起

過程雖然非常曲折,但是市面上能用的方案都用到了,嘗試過了,但是後面發現,極緻的優化,并不是1+1=2,要考慮業務的場景,因為一旦優化方案多了,他們之間的技術出發點,考慮的點可能會沖突。

這也是前端需要架構師的原因,開發重型應用如果前端有了一位架構師,那麼會少走很多彎路。

後端也是如此

Vue.js中的keep-alive使用:

Vue.js中,尤大大是這樣定義的:

clipboard.png

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

基礎使用:

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

大概思路:

clipboard.png

clipboard.png

切換也是非常平滑,沒有任何的閃屏

clipboard.png

特别提示: 這裡每個組件,下面還有一個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>
        )
    }
}

這樣這個組件你就可以在第二次需要渲染他的時候直接取緩存渲染了

下面是一組被緩存的一個組件,

clipboard.png

仔細看上面的注釋内容,再看當前body中多出來的div

clipboard.png

那麼他們是不是對應上了呢? 會是怎樣緩存渲染的呢?

到底怎麼緩存的

找到庫的源碼入口:

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> 會把在應用程序外面渲染的組件挂載到真正需要顯示的位置。

clipboard.png

這樣很明了了,原來如此

開始源碼:

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的結果:

clipboard.png

然後調用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的值
}

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

clipboard.png

接着是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();
  }

clipboard.png

這個邏輯到這裡并沒有完,我們需要進一步查看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);
}

老規矩,上圖解析源碼:

clipboard.png

很多人看起來雲裡霧裡,其實最終的實質就是通過了Coment組件的注釋,來查找到對應的需要渲染真實節點再進行替換,而這些節點都是緩存在内存中,DOM操作速度遠比框架對比後渲染快。這裡再次得到體現

這個庫,無論是否路由組件都可以使用,虛拟列表+緩存KeepAlive組件的Demo體驗地址

庫原鍊接地址為了項目安全,我自己重建了倉庫自己定制開發這個庫

感謝原先作者的貢獻 在我出現問題時候也第一時間給了我技術支持 謝謝!

新的庫名叫react-component-keepalive

直接可以在npm中找到

npm i react-component-keepalive

就可以正常使用了

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

從零編寫一個React框架

如何優化您的超大型React應用

歡迎關注我的前端公衆号: 前端巅峰

本人專注前端最前沿技術,跨平台重型應用開發,即時通訊等技術。

版本的後續計劃:

clipboard.png


如果覺得我的文章對你有用,請随意贊賞

你可能感興趣的

花雨傘 · 9月19日

首先點個贊,其次我想問下作者寫這篇文章花了多少時間呀?謝謝

回複

1

你好 4個小時不到的 主要是閱讀源碼時間

Peter譚金傑 作者 · 9月20日
0
花雨傘 · 9月20日
載入中...