vVirtal DOM主要包括如下三個方面html
snabbdom是一個優雅精簡的vdom庫,適合學習vdom思想和算法。下面的一切內容都是基於snabbdom.js的源碼。node
h 函數的主要功能是根據傳入的參數,返回一個VNode對象。git
根據snabbdom.js的h函數源碼來分析: snabbdom中對h函數作了重載,這是ts的特性。使得h函數能夠處理的狀況更加清晰,分爲如下四種:github
根據下面的源碼分析能夠看出,除了這四種狀況之外,對於SVG元素作了額外的處理,也就是添加了namespace。 最終都是調用vnode產生了一個VDOM節點。算法
/** * 重載h函數 * 根據選擇器 ,數據 ,建立 vnode */
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;
/** * h 函數比較簡單,主要是提供一個方便的工具函數,方便建立 vnode 對象 * @param sel 選擇器 * @param b 數據 * @param c 子節點 * @returns {{sel, data, children, text, elm}} */
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}, children: any, text: any, i: number;
// 若是存在子節點
// 三個參數的狀況 sel , data , children | text
if (c !== undefined) {
// 那麼h的第二項就是data
data = b;
// 若是c是數組,那麼存在子element節點
if (is.array(c)) { children = c; }
//不然爲子text節點
else if (is.primitive(c)) { text = c; }
// 說明c是一個子元素
else if (c && c.sel) { children = [c]; }
//若是c不存在,只存在b,那麼說明須要渲染的vdom不存在data部分,只存在子節點部分
} else if (b !== undefined) {
// 兩個參數的狀況 : sel , children | text
// 兩個參數的狀況 : sel , data
// 子元素數組
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) {
// 若是children是文本或數字 ,則建立文本節點
//{sel: sel, data: data, children: children, text: text, elm: elm, key: key};
//文本節點sel和data屬性都是undefined
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
// 針對svg的node進行特別的處理
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
// 增長 namespace
addNS(data, children, sel);
}
// 返回一個正常的vnode對象
return vnode(sel, data, children, text, undefined);
};
export default h;
複製代碼
vnode函數 很是簡單。僅僅是根據輸入參數返回了一個VNode類型的對象segmentfault
// 根據傳入的 屬性 ,返回一個 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
};
}
export default vnode;
複製代碼
下面是VNode的源碼:api
/** * 定義VNode類型 */
export interface VNode {
// 選擇器
sel: string | undefined;
// 數據,主要包括屬性、樣式、數據、綁定時間等
data: VNodeData | undefined;
// 子節點
children: Array<VNode | string> | undefined;
// 關聯的原生節點
elm: Node | undefined;
// 文本
text: string | undefined;
// key , 惟一值,爲了優化性能
key: Key | undefined;
}
複製代碼
另外還有一個比較重要的類型VNodeData.數組
/** * VNodeData節點所有都是可選屬性,也可動態添加任意類型的屬性 */
export interface VNodeData {
// vnode上的其餘屬性
// 屬性 能直訪問和接用
props?: Props;
// vnode上面的瀏覽器原生屬性,能夠使用setAttribute設置的
attrs?: Attrs;
//樣式類,class屬性集合
class?: Classes;
// style屬性集合
style?: VNodeStyle;
// vnode上面掛載的數據集合
dataset?: Dataset;
// 監聽事件集合
on?: On;
//
hero?: Hero;
// 額外附加的數據
attachData?: AttachData;
// 鉤子函數集合,執行到不一樣的階段調用不一樣的鉤子函數
hook?: Hooks;
//
key?: Key;
// 命名空間 SVGs 命名空間,主要用於SVG
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: Array<any>; // for thunks
//其它額外的屬性
[key: string]: any; // for any other 3rd party module
}
複製代碼
一切的一切都要從這個snabbdom.ts中的這個init方法開始。瀏覽器
按照層序的方式遍歷比較dom
對比的時候,只針對同級的嚴肅進行對比,減小算法複雜度。
爲了儘量不發生 DOM 的移動,會就近複用相同的 DOM 節點,複用的依據是判斷是不是同類型的 dom 元素
/** * * @param modules * @param domApi * @returns 返回 patch 方法 */
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, j: number, cbs = ({} as ModuleHooks);
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 循環 hooks , 將每一個 modules 下的 hook 方法提取出來存到 cbs 裏面
// 返回結果 eg : cbs['create'] = [modules[0]['create'],modules[1]['create'],...];
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);
}
}
}
function emptyNodeAt(elm: Element) {
...
}
// 建立一個刪除的回調,屢次調用這個回調,直到監聽器都沒了,就刪除元素
function createRmCb(childElm: Node, listeners: number) {
...
}
// 將 vnode 轉換成真正的 DOM 元素
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
...
}
// 添加 Vnodes 到 真實 DOM 中
function addVnodes(parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) {
...
}
function invokeDestroyHook(vnode: VNode) {
let i: any, j: number, data = vnode.data;
...
}
function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void {
...
}
function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
...省略函數
}
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
...省略函數體
}
// 返回patch 方法
/** * 觸發 pre 鉤子 * 若是老節點非 vnode, 則新建立空的 vnode * 新舊節點爲 sameVnode 的話,則調用 patchVnode 更新 vnode , 不然建立新節點 * 觸發收集到的新元素 insert 鉤子 * 觸發 post 鉤子 * @param oldVnode * @param vnode * @returns vnode */
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]();
// 若是老節點非 vnode , 則建立一個空的 vnode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 若是是同個節點,則進行修補
if (sameVnode(oldVnode, vnode)) {
// 進入patch流程
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 不一樣 Vnode 節點則新建
// as 是告訴類型檢查器,次數oldVnode.elm的類型應該是Node類型
elm = oldVnode.elm as Node;
//取到父節點node.parentNode屬性
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
// 插入新節點,刪除老節點
if (parent !== null) {
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 遍歷全部收集到的插入節點,調用插入的鉤子,
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
// 調用post的鉤子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
return patch;
}
複製代碼
從上面的代碼裏看init方法除了提取了create鉤子之外就是聲明瞭幾個重要的函數,而且返回了一個函數 patch。
patch函數只接受兩個參數,patch(oldVnode: VNode | Element, vnode: VNode)
,第一個參數oldNode能夠使VNode或者Element類型,第二個參數爲VNode類型。
聲明瞭一個insertedVnodeQueue,用來收集須要插入的元素隊列。 步驟以下:
若是oldVnode不是VNode類型,那麼調用emptyNodeAt建立一個空的VNode
若是oldNode和vnode是同一個節點,那麼直接進入patchVNode流程 patchVNode流程後面再詳細介紹
若是 不是同一個節點則先獲取oldVnode.elm的父DOM元素。將新元素插入到oldVnode.elm的下一個兄弟節點以前,而後移除oldVnode。其效果等同於使用新建立的元素替換了舊元素。
遍歷insertedVnodeQueue隊列,調用insert鉤子
調用post鉤子
返回vnode節點.
接下來的重點在於patchVnode。 前面定義的VNode結構類型,中包含了children和text兩個字段,這是爲了將元素子節點和文本分開處理
patchVnode函數只接受三個參數,patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue)
, 第一個參數oldNode是VNode類型, 第二個參數爲VNode類型。 第三個參數是插入的VNode隊列
patchVnode的主要邏輯以下:
updateChildren
流程,這個流程很重要很複雜,後面再說。上文中關鍵的地方在於 updateChildren
,這個過程處理新舊子元素數組的對比。 這裏就是diff算法的核心邏輯了。其實也很簡單。邏輯以下:
- (1)舊 vnode 頭 vs 新 vnode 頭(順序)
- (2)舊 vnode 尾 vs 新 vnode 尾(順序)
- (3)舊 vnode 頭 vs 新 vnode 尾(倒序)
- (4)舊 vnode 尾 vs 新 vnode 頭(倒序)
複製代碼
後面的源碼由於太長了就不貼了,有興趣的話就看這裏,歡迎你們批評指正。