日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

解密虚拟 DOM——snabbdom 核心源码解读

發布時間:2025/3/21 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 解密虚拟 DOM——snabbdom 核心源码解读 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文源碼地址:https://github.com/zhongdeming428/snabbdom

對很多人而言,虛擬 DOM 都是一個很高大上而且遠不可及的專有名詞,以前我也這么認為,后來在學習 Vue 源碼的時候發現 Vue 的虛擬 DOM 方案衍生于本文要講的 snabbdom 工具,經過閱讀源碼之后才發現,虛擬 DOM 原來就是這么回事,并沒有想象中那么難以理解嘛~

這篇文章呢,就單獨從 snabbdom 這個庫講起,不涉及其他任何框架,單獨從這個庫的源碼來聊一聊虛擬 DOM。

在講 snabbdom 之前,需要先學習 TypeScript 知識,以及 snabbdom 的基本使用方法。

一、snabbdom 核心概念

在學習 snabbdom 源碼之前,最好先學會用 snabbdom,至少要掌握 snabbdom 的核心概念,這是閱讀框架源碼之前基本都要做的準備工作。

以下內容可以直接到 snabbdom 官方文檔了解。

snabbdom 的一些優點

snabbdom 主要具有一下優點:

  • 核心部分的源代碼只有兩百多行(其實不止),容易讀懂。
  • 通過 modules 可以很容易地擴展。
  • 鉤子函數很豐富,用戶可以通過鉤子函數直接干涉 Vnode 到 DOM 掛載到最終銷毀的全過程。
  • 性能很棒。
  • 容易集成。

modules 的一些優點

  • 通過 h 函數,可以很容易地創建 Vnode。
  • 通過 h 函數可以創建 SVG 元素。
  • 事件處理能力強大。
  • 可以通過 Thunks 優化 DOM Diff 和事件。

第三方支持很多的優點

通過一些第三方的插件,可以很容易地支持 JSX、服務端 HTML 輸出等等……

核心 API

較為核心的 API 其實就四個:init、patch、 h和tovnode,通過這四個 API 就可以玩轉虛擬 DOM 啦!

下面簡單介紹一下這四個核心函數:

  • init:這是 snabbdom 暴露出來的一個核心函數,通過它我們才能開始使用許多重要的功能。該函數接受一個數組作為參數,數組內都是 module,通過 init 注冊了一系列要使用的 module 之后,它會給我們返回一個 patch 函數。

  • patch: 該函數是我們掛載或者更新 vnode 的重要途徑。它接受兩個參數,第一個參數可以是 HTML 元素或者 vnode,第二個元素只能是 vnode。通過 patch 函數,可以對第一個 vnode 進行更新,或者把 vnode 掛載/更新到 DOM 元素上。

  • tovnode: 用于把真實的 DOM 轉化為 vnode,適合把 SSR 生成的 DOM 轉化成 vnode,然后進行 DOM 操作。

  • h: 該函數用于創建 vnode,在許多地方都能見到它的身影。它接受三個參數:

    @param {string} selector|tag 標簽名或者選擇器 @param {object} data 數據對象,結構在后面講 @param {vNode[]|string} children 子節點,可以是文本節點

Module 模塊

Module 是 snabbdom 的一個核心概念,snabbdom 的核心主干代碼只實現了元素、id、class(不包含動態賦值)、元素內容(包括文本節點在內的子節點)這四個方面;而其他諸如 style 樣式、class 動態賦值、attr 屬性等功能都是通過 Module 擴展的,它們寫成了 snabbdom 的內部默認 Module,在需要的時候引用就行了。

那么 Module 究竟是什么呢?

snabbdom 的官方文檔已經講得很清楚了,Module 的本質是一個對象,對象的鍵由一些鉤子(Hooks)的名稱組成,鍵值都是函數,這些函數能夠在特定的 vnode/DOM 生命周期觸發,并接受規定的參數,能夠對周期中的 vnode/DOM 進行操作。

由于 snabbdom 使用 TypeScript 編寫,所以在之后看代碼的時候,我們可以非常清楚地看到 Module 的組成結構。

內置 Module 有如下幾種:

  • class:動態控制元素的 class。
  • props:設置 DOM 的一些屬性(properties)。
  • attributes:同樣用于設置 DOM 屬性,但是是 attributes,而且 properties。
  • style:設置 DOM 的樣式。
  • dataset:設置自定義屬性。
  • customProperties:CSS 的變量,使用方法參考官方文檔。
  • delayedProperties:延遲的 CSS 樣式,可用于創建動畫之類。

Hooks 鉤子

snabbdom 提供了豐富的生命周期鉤子:

鉤子名稱觸發時機Arguments to callback
prepatch 開始之前。none
init已經創建了一個 vnode。vnode
create已經基于 vnode 創建了一個 DOM,但尚未掛載。emptyVnode, vnode
insert創建的 DOM 被掛載了。vnode
prepatch一個元素即將被 patch。oldVnode, vnode
update元素正在被更新。oldVnode, vnode
postpatch元素已經 patch 完畢。oldVnode, vnode
destroy一個元素被直接或間接地移除了。間接移除的情況是指被移除元素的子元素。vnode
remove一個元素被直接移除了(卸載)。vnode, removeCallback
postpatch 結束。none

如何使用鉤子呢?

在創建 vnode 的時候,把定義的鉤子函數傳遞給 data.hook 就 OK 了;當然還可以在自定義 Module 中使用鉤子,同理定義鉤子函數并賦值給 Module 對象就可以了。

注意

Module 中只能使用以下幾種鉤子:pre,?create,?update,?destroy,?remove,?post。

而在 vnode 創建中定義的鉤子只能是以下幾種:init,?create,?insert,?prepatch,?update,?postpatch,?destroy,?remove。為什么 pre 和 post 不能使用呢?因為這兩個鉤子不在 vnode 的生命周期之中,在 vnode 創建之前,pre 已經執行完畢,在 vnode 卸載完畢之后,post 鉤子才開始執行。

EventListener

snabbdom 提供 DOM 事件處理功能,創建 vnode 時,定義好 data.on 即可。比如:

h('div',{on: {click: function() { /*...*/}}} )

如上,就定義了一個 click 事件處理函數。

那么如果我們要預先傳入一些自定義的參數那該怎么做呢?此時我們應該通過數組定義 handler:

h('div',{on: {click: [function(data) {/*...*/},data]}} )

那我們的事件對象如何獲取呢?這一點 snabbdom 已經考慮好了,event 對象和 vnode 對象會附加在我們的自定義參數后傳入到 handler。

Thunk

根據官方文檔的說明,Thunk 是一種優化策略,可以防止創建重復的 vnode,然后對實際未發生變化的 vnode 做替換或者 patch,造成不必要的性能損耗。在后面的源碼分析中,再做詳細說明吧。

二、源碼目錄結構

在首先查看源代碼之前,先分析一下源碼的目錄結構,好有的放矢的進行閱讀,下面是 src 目錄下的文件結構:

. ├── helpers │?? └── attachto.ts ├── hooks.ts // 定義了鉤子函數的類型 ├── htmldomapi.ts // 定義了一系列 DOM 操作的 API ├── h.ts // 主要定義了 h 函數 ├── is.ts // 主要定義了一個類型判斷輔助函數 ├── modules // 定義內置 module 的目錄 │?? ├── attributes.ts │?? ├── class.ts │?? ├── dataset.ts │?? ├── eventlisteners.ts │?? ├── hero.ts │?? ├── module.ts │?? ├── props.ts │?? └── style.ts ├── snabbdom.bundle.ts // 導出 h 函數和 patch 函數(注冊了所有內置模塊)。 ├── snabbdom.ts // 導出 init,允許自定義注冊模塊 ├── thunk.ts // 定義了 thunk ├── tovnode.ts // 定義了 tovnode 函數 └── vnode.ts // 定義了 vnode 類型2 directories, 18 files

所以看完之后,我們應該有了一個大致的概念,要較好的了解 vnode,我們可以先從 vnode 下手,結合文檔的介紹,可以詳細了解虛擬 DOM 的結構。

此外還可以從我們使用 snabbdom 的入口處入手,即 snabbdom.ts。

三、虛擬 DOM 結構

這一小節先了解 vnode 的結構是怎么樣的,由于 snabbdom 使用 TypeScript 編寫,所以關于變量的結構可以一目了然,打開 vnode.ts,可以看到關于 vnode 的定義:

export interface VNode {sel: string | undefined;data: VNodeData | undefined;children: Array<VNode | string> | undefined;elm: Node | undefined;text: string | undefined;key: Key | undefined; }

可以看到 vnode 的結構其實比較簡單,只有 6 個屬性。關于這六個屬性,官網已經做了介紹:

  • sel:是一種 CSS 選擇器,vnode 掛載為 DOM 時,會基于這個屬性構造 HTML 元素。
  • data:構造 vnode 的數據屬性,在構造 DOM 時會用到里面的數據,data 的結構在 vnode.ts 中可以找到定義,稍后作介紹。
  • children:這是一個 vnode 數組,在 vnode 掛載為 DOM 時,其 children 內的所有 vnode 會被構造為 HTML 元素,進一步掛載到上一級節點下。
  • elm:這是根據當前 vnode 構造的 DOM 元素。
  • text: 當前 vnode 的文本節點內容。
  • key:snabbdom 用 key 和 sel 來區分不同的 vnode,如果兩個 vnode 的 sel 和 key 屬性都相等,那么可以認為兩個 vnode 完全相等,他們之間的更新需要進一步比對。

往下翻可以看到 VNodeData 的類型定義:

export interface VNodeData {props?: Props;attrs?: Attrs;class?: Classes;style?: VNodeStyle;dataset?: Dataset;on?: On;hero?: Hero;attachData?: AttachData;hook?: Hooks;key?: Key;ns?: string; // for SVGsfn?: () => VNode; // for thunksargs?: Array<any>; // for thunks[key: string]: any; // for any other 3rd party module }

可以看出來這些屬性基本上都是在 Module 中所使用的,用于對 DOM 的一些數據、屬性進行定義,后面再進行介紹。

四、Hooks 結構

打開 hooks.ts,可以看到源碼如下:

import {VNode} from './vnode';export type PreHook = () => any; export type InitHook = (vNode: VNode) => any; export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any; export type InsertHook = (vNode: VNode) => any; export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any; export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any; export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any; export type DestroyHook = (vNode: VNode) => any; export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any; export type PostHook = () => any;export interface Hooks {pre?: PreHook;init?: InitHook;create?: CreateHook;insert?: InsertHook;prepatch?: PrePatchHook;update?: UpdateHook;postpatch?: PostPatchHook;destroy?: DestroyHook;remove?: RemoveHook;post?: PostHook; }

這些代碼定義了所有鉤子函數的結構類型(接受的參數、返回的參數),然后定義了 Hooks 類型,這與我們前面介紹的鉤子類型和所接受的參數是一致的。

五、Module 結構

打開 module.ts,看到源碼如下:

import {PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook} from '../hooks';export interface Module {pre: PreHook;create: CreateHook;update: UpdateHook;destroy: DestroyHook;remove: RemoveHook;post: PostHook; }

可以看到,該模塊先引用了上一節代碼定義的一系列鉤子的類型,然后用這些類型進一步定義了 Module。能夠看出來 module 實際上就是幾種鉤子函數組成的一個對象,用于干涉 DOM 的構造。

六、h 函數

h 函數是一個大名鼎鼎的函數,在各個框架中都有這個函數的身影。它的愿意是 hyperscript,意思是創造 HyperText 的 JavaScript,當然包括創造 HTML 的 JavaScript。在 snabbdom 中也不例外,h 函數旨在接受一系列參數,然后構造對應的 vnode,其返回的 vnode 最終會被渲染成 HTML 元素。

看看源代碼:

export function h(sel: string): VNode; export function h(sel: string, data: VNodeData): VNode; export function h(sel: string, children: VNodeChildren): VNode; export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode; export function h(sel: any, b?: any, c?: any): VNode {var data: VNodeData = {}, children: any, text: any, i: number;if (c !== undefined) {data = b;if (is.array(c)) { children = c; }else if (is.primitive(c)) { text = c; }else if (c && c.sel) { children = [c]; }} else if (b !== undefined) {if (is.array(b)) { children = b; }else if (is.primitive(b)) { text = b; }else if (b && b.sel) { children = [b]; }else { data = b; }}if (children !== undefined) {for (i = 0; i < children.length; ++i) {if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);}}if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&(sel.length === 3 || sel[3] === '.' || sel[3] === '#')) {addNS(data, children, sel);}return vnode(sel, data, children, text, undefined); }; export default h;

可以看到前面很大一段都是函數重載,所以不用太關注,只用關注到最后一行:

return vnode(sel, data, children, text, undefined);

在適配好參數之后,h函數調用了 vnode 函數,實現了 vnode 的創建,而 vnode 函數更簡單,就是一個工廠函數:

export function vnode(sel: string | undefined,data: any | undefined,children: Array<VNode | string> | undefined,text: string | undefined,elm: Element | Text | undefined): VNode {let key = data === undefined ? undefined : data.key;return {sel: sel, data: data, children: children,text: text, elm: elm, key: key}; }

它來自于 vnode.ts。

總之我們知道 h 函數接受相應的參數,返回一個 vnode 就行了。

七、snabbdom.ts

在講 snabbdom.ts 之前,本來應該先了解 htmldomapi.ts 的,但是這個模塊全都是對于 HTML 元素 API 的封裝,沒有講解的必要,所以閱讀本章之前,讀者自行閱讀 htmldomapi.ts 源碼即可。

這是整個項目的核心所在,也是定義入口函數的重要文件,這個文件大概有接近 400 行,主要定義了一些工具函數以及一個入口函數。

打開 snabbdom.ts ,最早看到的就是一些簡單的類型定義,我們也先來了解一下:

function isUndef(s: any): boolean { return s === undefined; } // 判斷 s 是否為 undefined。// 判斷 s 是否已定義(不為 undefined)。 function isDef(s: any): boolean { return s !== undefined; }// 一個 VNodeQueue 隊列,實際上是 vnode 數組,代表要掛載的 vnode。 type VNodeQueue = Array<VNode>;// 一個空的 vnode,用于傳遞給 craete 鉤子(查看第一節)。 const emptyNode = vnode('', {}, [], undefined, undefined);// 判斷兩個 vnode 是否重復,依據是 key 和 sel。 function sameVnode(vnode1: VNode, vnode2: VNode): boolean {return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }// 判斷是否是 vnode。 function isVnode(vnode: any): vnode is VNode {return vnode.sel !== undefined; }// 一個對象,用于映射 childen 數組中 vnode 的 key 和其 index 索引。 type KeyToIndexMap = {[key: string]: number};// T 是一個對象,其中的每一個鍵都被映射到 ArraysOf 類型,鍵值是 T 鍵值的數組集合。 type ArraysOf<T> = {[K in keyof T]: (T[K])[]; }// 參照上面的注釋。 type ModuleHooks = ArraysOf<Module>;

看完了基本類型的定義,可以繼續看 init 函數:

export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {let i: number, j: number, cbs = ({} as ModuleHooks);const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;for (i = 0; i < hooks.length; ++i) {cbs[hooks[i]] = [];for (j = 0; j < modules.length; ++j) {const hook = modules[j][hooks[i]];if (hook !== undefined) {(cbs[hooks[i]] as Array<any>).push(hook);}}}// 這中間定義了一大堆工具函數,稍后做選擇性分析……此處省略。// init 函數返回的 patch 函數,用于掛載或者更新 DOM。return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {let i: number, elm: Node, parent: Node;const insertedVnodeQueue: VNodeQueue = [];// 先執行完鉤子函數對象中的所有 pre 回調。for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();if (!isVnode(oldVnode)) {// 如果不是 VNode,那此時以舊的 DOM 為模板構造一個空的 VNode。oldVnode = emptyNodeAt(oldVnode);}if (sameVnode(oldVnode, vnode)) {// 如果 oldVnode 和 vnode 是同一個 vnode(相同的 key 和相同的選擇器),那么更新 oldVnode。patchVnode(oldVnode, vnode, insertedVnodeQueue);} else {// 如果 vnode 不同于 oldVnode,那么直接替換掉 oldVnode 對應的 DOM。elm = oldVnode.elm as Node;parent = api.parentNode(elm); // oldVnode 對應 DOM 的父節點。createElm(vnode, insertedVnodeQueue);if (parent !== null) {// 如果 oldVnode 的對應 DOM 有父節點,并且有同級節點,那就在其同級節點之后插入 vnode 的對應 DOM。api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));// 在把 vnode 的對應 DOM 插入到 oldVnode 的父節點內后,移除 oldVnode 的對應 DOM,完成替換。removeVnodes(parent, [oldVnode], 0, 0);}}for (i = 0; i < insertedVnodeQueue.length; ++i) {// 執行 insert 鉤子。因為 module 不包括 insert 鉤子,所以不必執行 cbs...(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);}// 執行 post 鉤子,代表 patch 操作完成。for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();// 最終返回 vnode。return vnode;}; }

可以看到 init 函數其實不僅可以接受一個 module 數組作為參數,還可以接受一個 domApi 作為參數,這在官方文檔上是沒有說明的。可以理解為 snabbdom 允許我們自定義 dom 的一些操作函數,在這個過程中對 DOM 的構造進行干預,只需要我們傳遞的 domApi 的結構符合預定義就可以了,此處不再細表。

然后可以看到的就是兩個嵌套著的循環,大致意思是遍歷 hooks 和 modules,構造一個 ModuleHooks 類型的 cbs 變量,那這是什么意思呢?

hooks 定義如下:

const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

那就是把每個 module 中對應的鉤子函數整理到 cbs 鉤子名稱對應的數組中去,比如:

const module1 = {create() { /*...*/ },update() { /*...*/ } }; const module2 = {create() { /*...*/ },update() { /*...*/ } }; // 經過整理之后…… // cbs 如下: {create: [create1, create2],update: [update1, update2] }

這種結構類似于發布——訂閱模式的事件中心,以事件名作為鍵,鍵值是事件處理函數組成的數組,在事件發生時,數組中的函數會依次執行,與此處一致。

在處理好 hooks 之后,init 內部定義了一系列工具函數,此處暫不講解,先往后看。

init 處理到最后返回的使我們預期的 patch 函數,該函數是我們使用 snabbdom 的重要入口,其具體定義如下:

// init 函數返回的 patch 函數,用于掛載或者更新 DOM。 return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {let i: number, elm: Node, parent: Node;const insertedVnodeQueue: VNodeQueue = [];// 先執行完鉤子函數對象中的所有 pre 回調。for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();if (!isVnode(oldVnode)) {// 如果不是 VNode,那此時以舊的 DOM 為模板構造一個空的 VNode。oldVnode = emptyNodeAt(oldVnode);}if (sameVnode(oldVnode, vnode)) {// 如果 oldVnode 和 vnode 是同一個 vnode(相同的 key 和相同的選擇器),那么更新 oldVnode。patchVnode(oldVnode, vnode, insertedVnodeQueue);} else {// 如果 vnode 不同于 oldVnode,那么直接替換掉 oldVnode 對應的 DOM。elm = oldVnode.elm as Node;parent = api.parentNode(elm); // oldVnode 對應 DOM 的父節點。createElm(vnode, insertedVnodeQueue);if (parent !== null) {// 如果 oldVnode 的對應 DOM 有父節點,并且有同級節點,那就在其同級節點之后插入 vnode 的對應 DOM。api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));// 在把 vnode 的對應 DOM 插入到 oldVnode 的父節點內后,移除 oldVnode 的對應 DOM,完成替換。removeVnodes(parent, [oldVnode], 0, 0);}}for (i = 0; i < insertedVnodeQueue.length; ++i) {// 執行 insert 鉤子。因為 module 不包括 insert 鉤子,所以不必執行 cbs...(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);}// 執行 post 鉤子,代表 patch 操作完成。for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();// 最終返回 vnode。return vnode; };

可以看到在 patch 執行的一開始,就遍歷了 cbs 中的所有 pre 鉤子,也就是所有 module 中定義的 pre 函數。執行完了 pre 鉤子,代表 patch 過程已經開始了。

接下來首先判斷 oldVnode 是不是 vnode 類型,如果不是,就代表 oldVnode 是一個 HTML 元素,那我們就要把他轉化為一個 vnode,方便后面的更新,更新完畢之后再進行掛載。轉化為 vnode 的方式很簡單,直接將其 DOM 結構掛載到 vnode 的 elm 屬性,然后構造好 sel 即可。

隨后,通過 sameVnode 判斷是否是同一個 “vnode”。如果不是,那么就可以直接把兩個 vnode 代表的 DOM 元素進行直接替換;如果是“同一個” vnode,那么就需要進行下一步對比,看看到底有哪些地方需要更新,可以看做是一個 DOM Diff 過程。所以這里出現了 snabbdom 的一個小訣竅,通過 sel 和 key 區分 vnode,不相同的 vnode 可以直接替換,不進行下一步的替換。這樣做在很大程度上避免了一些沒有必要的比較,節約了性能。

完成上面的步驟之后,就已經把 vnode 掛載到 DOM 上了,完成這個步驟之后,需要執行 vnode 的 insert 鉤子,告訴所有的模塊:一個 DOM 已經掛載了!

最后,執行所有的 post 鉤子并返回 vnode,通知所有模塊整個 patch 過程已經結束啦!

不難發現重點在于當 oldVnode 和 vnode 是同一個 vnode 時如何進行更新。這就自然而然的涉及到了 patchVnode 函數,該函數結構如下:

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {let i: any, hook: any;if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {// 如果 vnode.data.hook.prepatch 不為空,則執行 prepatch 鉤子。i(oldVnode, vnode);}const elm = vnode.elm = (oldVnode.elm as Node);let oldCh = oldVnode.children;let ch = vnode.children;// 如果兩個 vnode 是真正意義上的相等,那完全就不用更新了。if (oldVnode === vnode) return;if (vnode.data !== undefined) {// 如果 vnode 的 data 不為空,那么執行 update。for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);i = vnode.data.hook;// 執行 vnode.data.hook.update 鉤子。if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);}if (isUndef(vnode.text)) {// 如果 vnode.text 未定義。if (isDef(oldCh) && isDef(ch)) {// 如果都有 children,那就更新 children。if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);} else if (isDef(ch)) {// 如果 oldVnode 是文本節點,而更新后 vnode 包含 children;// 那就先移除 oldVnode 的文本節點,然后添加 vnode。if (isDef(oldVnode.text)) api.setTextContent(elm, '');addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);} else if (isDef(oldCh)) {// 如果 oldVnode 有 children,而新的 vnode 只有文本節點;// 那就移除 vnode 即可。removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);} else if (isDef(oldVnode.text)) {// 如果更新前后,vnode 都沒有 children,那么就添加空的文本節點,因為大前提是 vnode.text === undefined。api.setTextContent(elm, '');}} else if (oldVnode.text !== vnode.text) {// 定義了 vnode.text,并且 vnode 的 text 屬性不同于 oldVnode 的 text 屬性。if (isDef(oldCh)) {// 如果 oldVnode 具有 children 屬性(具有 vnode),那么移除所有 vnode。removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);}// 設置文本內容。api.setTextContent(elm, vnode.text as string);}if (isDef(hook) && isDef(i = hook.postpatch)) {// 完成了更新,調用 postpatch 鉤子函數。i(oldVnode, vnode);} }

該函數是用于更新 vnode 的主要函數,所以 vnode 的主要生命周期都在這個函數內完成。首先執行的鉤子就是 prepatch,表示元素即將被 patch。然后會判斷 vnode 是否包含 data 屬性,如果包含則說明需要先更新 data,這時候會調用所有的 update 鉤子(包括模塊內的和 vnode 自帶的 update 鉤子),在 update 鉤子內完成 data 的合并更新。在 children 更新之后,還會調用 postpatch 鉤子,表示 patch 過程已經執行完畢。

接下來從 text 入手,這一大塊的注釋都在代碼里面寫得很清楚了,這里不再贅述。重點在于 oldVnode 和 vnode 都有 children 屬性的時候,如何更新 children?接下來看 updateChildren:

function updateChildren(parentElm: Node,oldCh: Array<VNode>,newCh: Array<VNode>,insertedVnodeQueue: VNodeQueue) {let oldStartIdx = 0, newStartIdx = 0;let oldEndIdx = oldCh.length - 1;let oldStartVnode = oldCh[0];let oldEndVnode = oldCh[oldEndIdx];let newEndIdx = newCh.length - 1;let newStartVnode = newCh[0];let newEndVnode = newCh[newEndIdx];let oldKeyToIdx: any;let idxInOld: number;let elmToMove: VNode;let before: any;// 從兩端開始開始遍歷 children。while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (oldStartVnode == null) {oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left} else if (oldEndVnode == null) {oldEndVnode = oldCh[--oldEndIdx];} else if (newStartVnode == null) {newStartVnode = newCh[++newStartIdx];} else if (newEndVnode == null) {newEndVnode = newCh[--newEndIdx];} else if (sameVnode(oldStartVnode, newStartVnode)) { // 如果是同一個 vnode。patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); // 更新舊的 vnode。oldStartVnode = oldCh[++oldStartIdx];newStartVnode = newCh[++newStartIdx];} else if (sameVnode(oldEndVnode, newEndVnode)) { // 同上,但是是從尾部開始的。patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);oldEndVnode = oldCh[--oldEndIdx];newEndVnode = newCh[--newEndIdx];} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));oldStartVnode = oldCh[++oldStartIdx];newEndVnode = newCh[--newEndIdx];} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);oldEndVnode = oldCh[--oldEndIdx];newStartVnode = newCh[++newStartIdx];} else {if (oldKeyToIdx === undefined) {// 創造一個 hash 結構,用鍵映射索引。oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);}idxInOld = oldKeyToIdx[newStartVnode.key as string]; // 通過 key 來獲取對應索引。if (isUndef(idxInOld)) { // New element// 如果找不到索引,那就是新元素。api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);newStartVnode = newCh[++newStartIdx];} else {// 找到對應的 child vnode。elmToMove = oldCh[idxInOld];if (elmToMove.sel !== newStartVnode.sel) {// 如果新舊 vnode 的選擇器不能對應,那就直接插入到舊 vnode 之前。api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);} else {// 選擇器匹配上了,可以直接更新。patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);oldCh[idxInOld] = undefined as any; // 已更新的舊 vnode 賦值為 undefined。api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);}newStartVnode = newCh[++newStartIdx];}}}if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {// 沒匹配上的多余的就直接插入到 DOM 咯。if (oldStartIdx > oldEndIdx) {// newCh 里面有新的 vnode,直接插入到 DOM。before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);} else {// newCh 里面的 vnode 比 oldCh 里面的少,說明有元素被刪除了。removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);}} }

updateVnode 函數在一開始就從 children 數組的首尾兩端開始遍歷。可以看到在遍歷開始的時候會有一堆的 null 判斷,為什么呢?因為后面會把已經更新的 vnode children 賦值為 undefined。

判斷完 null 之后,會比較新舊 children 內的節點是否“相同”(排列組合共有四種比較方式),如果相同,那就繼續調用 patchNode 更新節點,更新完之后就可以插入 DOM 了;如果四中情況都匹配不到,那么就通過之前建立的 key 與索引之間的映射來尋找新舊 children 數組中對應 child vnode 的索引,找到之后再進行具體操作。關于具體的操作,代碼中已經注釋了~

對于遍歷之后多余的 vnode,再分情況進行比較;如果 oldCh 多于 newCh,那說明該操作刪除了部分 DOM。如果 oldCh 少于 newCh,那說明有新增的 DOM。

關于 updateChildren 函數的講述,這篇文章的講述更為詳細:vue的Virtual Dom實現- snabbdom解密 ,大家可以去讀一下~

講完最重要的這個函數,整個核心部分基本上是弄完了,不難發現 snabbdom 的秘訣就在于使用:

  • 使用虛擬 DOM 模擬真實 DOM,JavaScript 內存操作性能大大優于 DOM 操作,所以性能比較好。
  • Diff 算法比較好,只比較同級 vnode,不會循環遍歷去比較,而且采用 key 和 sel 標記 vnode,大大優化比較速度。這一做法類似于 Immutable,使用 hash 比較代替對象的循環遞歸比較,大大降低時間復雜度。

最后還有一個小問題,這個貫穿許多函數的 insertedVnodeQueue 數組是干嘛的?它只在 createElm 函數中進行 push 操作,然后在最后的 insert 鉤子中進行遍歷。仔細一想就可以發現,這個插入 vnode 隊列存起來的是一個 children 的左右子 children,看下面一段代碼:

h('div',{},[h(/*...*/),h(/*...*/),h(/*...*/)] )

可以看到 div 下面包含了三個 children,那么當這個 div 元素被插入到 DOM 時,它的三個子 children 也會觸發 insert 事件,所以在插入 vnode 時,會遍歷其所有 children,然后每個 vnode 都會放入到隊列中,在插入之后再統一執行 insert 鉤子。

以上,就寫這么多吧~多的也沒時間寫了。

八、參考文章

  • vue的Virtual Dom實現- snabbdom解密

轉載于:https://www.cnblogs.com/DM428/p/10779338.html

總結

以上是生活随笔為你收集整理的解密虚拟 DOM——snabbdom 核心源码解读的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。