iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0
Vue.js

淺談vue3源碼,很淺的那種系列 第 14

[Day 14] runtime-core——比對新舊虛擬dom

  • 分享至 

  • xImage
  •  

都看我的鐵人賽整整兩個禮拜了,還不離不棄的應該都是鐵粉了。我在文章裡偷偷吐點苦水,應該不會有人介意吧:3

以原神來比喻吧,我覺得我是像達達利亞那種類型的人。我渴望與幹起來有手感的需求廝殺,把所有雜念都拋諸腦後,一心一意只沉浸於眼前的戰鬥,然後在一番苦戰之後把需求踩在腳下,享受那種征服的感覺。

唉但是吧,我總感覺目前的工作……細節我不能說,籠統來說就是既沒辦法沒有雜念,也沒有夠格稱作廝殺的需求。

當然我也知道,「受限於環境」不過是放棄成長的藉口,所以我在工作上給自己加了許多額外的需求。簡單的有借鑒lru算法的思想實現api緩存策略、在vite專案復刻nuxt的useCookie和useAsyncData,複雜的有做一個能代替我寫後台的後台。

但道理我都懂,理性上都是懂的,感性上卻難免忍不住去想,如果我換了一個環境,有沒有可能就能和真正有價值的需求廝殺,能夠做出一些更有意義的事。

又或者其實我只是覺得鄰居的草皮比較綠、外國的月亮比較圓,我需要的根本不是換環境而是知足?我迷惘,不知道自己該做甚麼,也不知道自己想要甚麼。


哎說多了,咱還是回歸正題,比對虛擬dom。

前置準備

先前我們在寫/src/runtime-core/vnode.ts時漏了兩個地方,一個是在宣告Text那邊應該多補一行:

export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');

在我也忘記是vue3.幾版以前,template裡面最外層的元素(方便起見,以下稱為根標籤)只能有一個。學習到這邊也不難理解,畢竟我們需要將虛擬dom掛載到實體dom的_vnode屬性上,如果要讓一個組件有不只一個根標籤,它外層的實體dom的_vnode屬性就得掛載多個虛擬dom。

所以在vue的某次改版,提出了Fragment的概念——當某個組件的template中有不只一個根標籤時,就用一個type為Fragment的假的虛擬dom節點把它們包起來。所以當虛擬dom的type是Fragment,就代表它是包裹組件中多個根標籤的虛假標籤。

另外我們也在vnode.ts的最下方暴露一個方法:

export createVNode = (type: string | symbol, props: Record<string, any>, children: Array<VNode | string> | string = null) => {
  const shapeFlag = typeof type === 'string' ? ShapeFlags.ELEMENT : 0;

  const vnode: VNode = {
    type,
    props,
    children,
    el: null,
    key: props?.key,
    __v_isVnode: true,
    shapeFlag
  };

  if (children) {
    let type;
    if (Array.isArray(children)) type = ShapeFlags.ARRAY_CHILDREN;
    else {
      children = String(children);
      type = ShapeFlags.TEXT_CHILDREN;
    }
    vnode.shapeFlag |= type;
  }
  return vnode;
}

用以回傳一個新創建的虛擬dom節點。

相同節點

除了上述兩點,我們還可以再多暴露一個isSameVnode方法,方便我們判斷當數據更新時,新舊虛擬dom節點是否相同。
為此,首先我們必須先探討一個議題——如何定義節點是否相同。

判斷地址是否相同是絕對不合適的。即使參數完全相同,透過上述createVNode()創建的兩個節點的地址必定不同,但我們判斷節點是否相同是為了決定要不要重新渲染這個dom元素節點,如果標籤相同、屬性相同、內文相同,除了地址以外的一切都相同,那重新渲染它就純屬浪費效能了。

說到底只要是相同標籤,屬性或內文的改變都不需要重新渲染這個dom元素,修改它的屬性或內文即可。因此我們可以定義所謂的節點相同,就是這兩個節點的標籤名以及key相同,並完成這個方法:

export function isSameVnode = (oldNode: VNode, newNode: VNode) => {
  return (oldNode.type === newNode.type) && (oldNode.key === newNode.key);
}

patch

做完前置準備,我們就能回到/src/runtime-core/renderer.ts,把上面寫的東西都引入了,然後繼續完成render方法中的patch了。

import { Text, Fragment, VNode, ShapeFlags, isSameVnode } from "./vnode";

// ......
export const createRenderer = (renderOptions: RenderOptions) => {
  // ......
  
  const patch = (oldNode: VNode, newNode: VNode, container: HTMLElement, anchor: HTMLElement | Text = null) => {
    if (oldNode === newNode) return;

    if (oldNode && !isSameVnode(oldNode, newNode)) {
      unmount(oldNode);
      oldNode = null;
    }

    // type可為標籤名或Text代表僅渲染純文字,shapeFlag以二進位存儲虛擬dom特徵
    // (例如children是陣列的HTML元素:10001,參考 shared/index.ts -> ShapeFlags)
    const { type, shapeFlag } = newNode;

    switch (type) {
      case Text:
        processText(oldNode, newNode, container);
        break;
      case Fragment:
        processFragment(oldNode, newNode, container);
        break;
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) processElement(oldNode, newNode, container, anchor);
    }
  };
  
  // ......
}

如果新舊節點的地址完全相同,甚麼也不用做直接return。

if (oldNode === newNode) return;

如果新舊節點不是相同節點(意即不同標籤或不同key),就重新渲染舊的節點。

if (oldNode && !isSameVnode(oldNode, newNode)) {
  unmount(oldNode);
  oldNode = null;
}

之後再用switch(type)判斷這個新節點是文本、Fragment還是dom元素節點,明天我們將根據不同的情況,來做不同的處理。

githubmain分支commit「[Day 14] runtime-core——比對新舊虛擬dom」


上一篇
[Day 13] runtime-core——render方法
下一篇
[Day 15] runtime-core——渲染dom元素
系列文
淺談vue3源碼,很淺的那種31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言