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

  • 2019 年 10 月 5 日
  • 筆記

現代框架的本質其實還是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圖,不知道為什存後切換也是非常平滑,沒有任何的閃屏

特別提示: 這裡每個組件,下面還有一個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即是傳入給Provider的children

一開始我們看見的快取組件內容顯示的都是一個注釋內容 那為什麼可以渲染出東西來呢

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函數監聽的自定義事件為了觸發componentWillUnmount和componentWillUnactivate

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體驗地址

庫原鏈接地址為了項目安全,我自己重建了倉庫自己訂製開發這個庫

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

新的庫名叫react-component-keepalive

直接可以在npm中找到

npm i react-component-keepalive

就可以正常使用了

Tags: