本文源碼地址:https://github.com/zhongdeming428/snabbdomhtml
對不少人而言,虛擬 DOM 都是一個很高大上並且遠不可及的專有名詞,之前我也這麼認爲,後來在學習 Vue 源碼的時候發現 Vue 的虛擬 DOM 方案衍生於本文要講的 snabbdom 工具,通過閱讀源碼以後才發現,虛擬 DOM 原來就是這麼回事,並無想象中那麼難以理解嘛~vue
這篇文章呢,就單獨從 snabbdom 這個庫講起,不涉及其餘任何框架,單獨從這個庫的源碼來聊一聊虛擬 DOM。node
在講 snabbdom 以前,須要先學習 TypeScript 知識,以及 snabbdom 的基本使用方法。git
在學習 snabbdom 源碼以前,最好先學會用 snabbdom,至少要掌握 snabbdom 的核心概念,這是閱讀框架源碼以前基本都要作的準備工做。github
如下內容能夠直接到 snabbdom 官方文檔瞭解。算法
snabbdom 主要具備一下優勢:typescript
modules
能夠很容易地擴展。經過一些第三方的插件,能夠很容易地支持 JSX、服務端 HTML 輸出等等……api
較爲核心的 API 其實就四個:init
、patch
、 h
和tovnode
,經過這四個 API 就能夠玩轉虛擬 DOM 啦!數組
下面簡單介紹一下這四個核心函數:bash
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 是 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 樣式,可用於建立動畫之類。snabbdom 提供了豐富的生命週期鉤子:
鉤子名稱 | 觸發時機 | Arguments to callback |
---|---|---|
pre |
patch 開始以前。 | 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 |
post |
patch 結束。 | 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 鉤子纔開始執行。
snabbdom 提供 DOM 事件處理功能,建立 vnode 時,定義好 data.on
便可。好比:
h( 'div', { on: { click: function() { /*...*/} } } )
如上,就定義了一個 click 事件處理函數。
那麼若是咱們要預先傳入一些自定義的參數那該怎麼作呢?此時咱們應該經過數組定義 handler:
h( 'div', { on: { click: [ function(data) {/*...*/}, data ] } } )
那咱們的事件對象如何獲取呢?這一點 snabbdom 已經考慮好了,event 對象和 vnode 對象會附加在咱們的自定義參數後傳入到 handler。
根據官方文檔的說明,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。
這一小節先了解 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 SVGs fn?: () => VNode; // for thunks args?: Array<any>; // for thunks [key: string]: any; // for any other 3rd party module }
能夠看出來這些屬性基本上都是在 Module 中所使用的,用於對 DOM 的一些數據、屬性進行定義,後面再進行介紹。
打開 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.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 以前,原本應該先了解 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 right patchVnode(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 left patchVnode(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 的祕訣就在於使用:
最後還有一個小問題,這個貫穿許多函數的 insertedVnodeQueue
數組是幹嗎的?它只在 createElm
函數中進行 push 操做,而後在最後的 insert 鉤子中進行遍歷。仔細一想就能夠發現,這個插入 vnode 隊列存起來的是一個 children 的左右子 children,看下面一段代碼:
h( 'div', {}, [ h(/*...*/), h(/*...*/), h(/*...*/) ] )
能夠看到 div 下面包含了三個 children,那麼當這個 div 元素被插入到 DOM 時,它的三個子 children 也會觸發 insert 事件,因此在插入 vnode 時,會遍歷其全部 children,而後每一個 vnode 都會放入到隊列中,在插入以後再統一執行 insert 鉤子。
以上,就寫這麼多吧~多的也沒時間寫了。