都看我的鐵人賽整整兩個禮拜了,還不離不棄的應該都是鐵粉了。我在文章裡偷偷吐點苦水,應該不會有人介意吧: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);
}
做完前置準備,我們就能回到/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」