vue系列---snabbdom.js使用及原始碼分析(九)
一:什麼是snabbdom?
在學習Vue或React中,我們瞭解最多的就是虛擬DOM,虛擬DOM可以看作是一顆模擬了DOM的Javascript樹,主要是通過vnode實現一個無狀態的元件,當元件狀態發生變更時,就會觸發 virtual-dom 資料的變化,然後使用虛擬節點樹進行渲染,但是在渲染之前,會使用新生成的虛擬節點樹和上一次生成的虛擬節點樹進行對比,只渲染兩者之間不同的部分。
為什麼我們需要虛擬DOM呢?
在web很早時期,我們使用jquery來做頁面的互動,比如如下排序這麼一個demo。程式碼如下:
<!DOCTYPE html> <html> <head> <title></title> <meta charset="utf-8"> <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script> </head> <body> <div id="app"> </div> <div id="sort" style="margin-top: 20px;">按年紀排序</div> <script type="text/javascript"> var datas = [ { 'name': 'kongzhi11', 'age': 32 }, { 'name': 'kongzhi44', 'age': 29 }, { 'name': 'kongzhi22', 'age': 31 }, { 'name': 'kongzhi33', 'age': 30 } ]; var render = function() { var html = ''; datas.forEach(function(item, index) { html += `<li> <div class="u-cls"> <span class="name">姓名:${item.name}</span> <span class="age" style="margin-left:20px;">年齡:${item.age}</span> <span class="closed">x</span> </div> </li>`; }); return html; }; $("#app").html(render()); $('#sort').on('click', function() { datas = datas.sort(function(a, b) { return a.age - b.age; }); $('#app').html(render()); }) </script> </body> </html>
如上demo排序,雖然在使用jquery時代這種方式是可行的,我們點選按鈕,它就可以從小到大的排序,但是它比較暴力,它會將之前的dom全部刪除,然後重新渲染新的dom節點,我們知道,操作DOM會影響頁面的效能,並且有時候資料根本就沒有發生改變,我們希望未更改的資料不需要重新渲染操作。因此虛擬DOM的思想就出來了,虛擬DOM的思想是先控制資料再到檢視,但是資料狀態是通過diff比對,它會比對新舊虛擬DOM節點,然後找出兩者之前的不同,然後再把不同的節點再發生渲染操作。
如下圖演示:
snabbdom 是虛擬DOM的一種簡單的實現,並且在Vue中實現的虛擬DOM是借鑑了 snabbdom.js 的,因此我們這邊首先來學習該庫。
如果我們自己需要實現一個虛擬DOM,我們一般有如下三個步驟需要完成:
1. compile, 我們如何能把真實的DOM編譯成Vnode。
2. diff. 我們怎麼樣知道oldVnode和newVnode之間的不同。
3. patch. 通過第二步的比較知道不同點,然後把不同的虛擬DOM渲染到真實的DOM上去。
snabbdom庫我們可以到github原始碼下載一份,github地址為:https://github.com/snabbdom/snabbdom/tree/8079ba78685b0f0e0e67891782c3e8fb9d54d5b8,我這邊下載的是0.5.4版本的,因為從v0.6.0版本之上使用的是 typescript編寫的,對於沒有使用過typescript人來說,理解起來可能並不那麼順利,因此我這邊就來分析下使用javascript編寫的程式碼。
注意:不管新版本還是前一個版本,內部的基本原理是類似的。新增的版本可能會新增一些新功能。但是不影響我們理解主要的功能。
我們從github上可以看到,snabbdom 有很多tag,我們把專案下載完成後,我們切換到v0.5.4版本即可。
專案的整個目錄結構如下:
|--- snabbdom | |--- dist | |--- examples | |--- helpers | |--- modules | | |--- attributes.js | | |--- class.js | | |--- dataset.js | | |--- eventlisteners.js | | |--- hero.js | | |--- props.js | | |--- style.js | |--- perf | |--- test | |--- h.js | |--- htmldomapi.js | |--- is.js | |--- snabbdom.js | |--- thunk.js | |--- vnode.js
snabbdom/dist: 包含了snabbdom打包後的檔案。
snabbdom/examples: 包含了使用snabbdom的列子。
snabbdom/helpers: 包含svg操作需要的工具。
snabbdom/modules: 包含了 attributes, props, class, dataset, eventlinsteners, style, hero等操作。
snabbdom/perf: 效能測試
snabbdom/test: 測試用例相關的。
snabbdom/h.js: 把狀態轉化為vnode.
snabbdom/htmldomapi.js: 原生dom操作
snabbdom/is.js: 判斷型別操作。
snabbdom/snabbdom.js: snabbdom核心,包括diff、patch, 及虛擬DOM構建DOM的過程等
snabbdom/thunk.js: snabbdom下的thunk的功能實現。
snabbdom/vnode.js: 構造vnode。
snabbdom 主要的介面有:
1、 h(type, data, children),返回 Virtual DOM 樹。
2、patch(oldVnode, newVnode),比較新舊 Virtual DOM 樹並更新。
在npm庫中,我們也可以看到snabbdom庫的基本使用,請看地址:https://www.npmjs.com/package/snabbdom
因此我們可以按照npm庫中demo列子,可以自己簡單做一個demo,當然我們需要搭建一個簡單的webpack打包環境即可(環境可以簡單的搭建下即可,這裡不多介紹哦。),在入口js檔案中,我們引入 snabbdom 這個庫,然後在入口檔案的js中新增如下程式碼:
var snabbdom = require('snabbdom'); var patch = snabbdom.init([ require('snabbdom/modules/class'), require('snabbdom/modules/props'), require('snabbdom/modules/style'), require('snabbdom/modules/eventlisteners') ]); /* h 是一個生成vnode的包裝函式 */ var h = require('snabbdom/h'); // 構造一個虛擬dom var vnode = h('div#app', {style: {color: '#000'}}, [ h('span', {style: {fontWeight: 'bold'}}, "my name is kongzhi"), ' and xxxx', h('a', {props: {href: '/foo'}}, '我是空智') ] ); // 初始化容器 var app = document.getElementById('app'); // 將vnode patch 到 app 中 patch(app, vnode); // 建立一個新的vnode var newVnode = h('div#app', {style: {color: 'red'}}, [ h('span', {style: {fontWeight: 'normal'}}, "my name is tugenhua"), ' and yyyyy', h('a', {props: {href: '/bar'}}, '我是空智22') ] ); // 將新的newVnode patch到vnode中 patch(vnode, newVnode);
注意:我們這邊的snabbdom是v0.5.4版本的,可能和npm包中程式碼引用方式稍微有些差別,但是並不影響使用。
當然我們index.html模板頁面需要有一個 div 元素,id為app 這樣的,如下模板程式碼:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <div id="app"></div> </body> </html>
然後我們打包後,執行該頁面,可以看到頁面被渲染出來了。然後我們頁面中的html程式碼會被渲染成如下:
<div id="app" style="color: red;"> <span style="font-weight: normal;">my name is tugenhua</span> and yyyyy<a href="/bar">我是空智22</a> </div>
為什麼會被渲染成這樣的呢?我們來一步步分析下上面js的程式碼:
先是引入 snabbdom庫,然後呼叫該庫的init方法,基本程式碼如下所示:
var snabbdom = require('snabbdom'); var patch = snabbdom.init([ require('snabbdom/modules/class'), require('snabbdom/modules/props'), require('snabbdom/modules/style'), require('snabbdom/modules/eventlisteners') ]);
因此我們需要把目光轉移到 snabbdom/snabbdom.js 中,基本程式碼如下:
var VNode = require('./vnode'); var is = require('./is'); var domApi = require('./htmldomapi'); ..... 更多程式碼
在snabbdom.js程式碼中引入瞭如上三個庫,因此在分析 snabbdom.js 程式碼之前,我們先看下如上三個庫做了什麼事情。
先看 snabbdom/vnode.js 程式碼如下:
/* * VNode函式如下:主要的功能是構造VNode, 把輸入的引數轉化為Vnode * @param {sel} 選擇器,比如 'div#app' 或 'span' 這樣的等等 * @param {data} 對應的是Vnode繫結的資料,可以是如下型別:attribute、props、eventlistener、 class、dataset、hook 等這樣的。 * @param {children} 子節點陣列 * @param {text} 當前的text節點內容 * @param {elm} 對真實的dom element的引用 * @return {sel: *, data: *, children: *, text: *, elm: *, key: undefined } * 如下返回的key, 作用是用於不同Vnode之間的比對 */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
我們再把目光轉移到 snabbdom/is.js中,基本的程式碼如下所示:
module.exports = { array: Array.isArray, primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; }, };
該程式碼中匯出了 array 判斷是不是一個數組,primitive的作用是判斷是不是一個字串或數字型別的。
接著我們把目光再轉移到 snabbdom/htmldomapi.js 中,基本程式碼如下:
function createElement(tagName){ return document.createElement(tagName); } function createElementNS(namespaceURI, qualifiedName){ return document.createElementNS(namespaceURI, qualifiedName); } function createTextNode(text){ return document.createTextNode(text); } function insertBefore(parentNode, newNode, referenceNode){ parentNode.insertBefore(newNode, referenceNode); } function removeChild(node, child){ node.removeChild(child); } function appendChild(node, child){ node.appendChild(child); } function parentNode(node){ return node.parentElement; } function nextSibling(node){ return node.nextSibling; } function tagName(node){ return node.tagName; } function setTextContent(node, text){ node.textContent = text; } module.exports = { createElement: createElement, createElementNS: createElementNS, createTextNode: createTextNode, appendChild: appendChild, removeChild: removeChild, insertBefore: insertBefore, parentNode: parentNode, nextSibling: nextSibling, tagName: tagName, setTextContent: setTextContent };
如上程式碼,我們可以看到 htmldomapi.js 中提供了對原生dom操作的一層抽象。看看程式碼就能理解了。
現在我們可以看我們的如下程式碼了:
var patch = snabbdom.init([ require('snabbdom/modules/class'), require('snabbdom/modules/props'), require('snabbdom/modules/style'), require('snabbdom/modules/eventlisteners') ]);
snabbdom/modules/class.js 程式碼如下:
function updateClass(oldVnode, vnode) { ... 更多程式碼 } module.exports = {create: updateClass, update: updateClass};
snabbdom/modules/props.js 程式碼如下:
function updateProps(oldVnode, vnode) { ... 更多程式碼 } module.exports = {create: updateProps, update: updateProps};
snabbdom/modules/style.js 程式碼如下:
function updateStyle(oldVnode, vnode) { ... 更多程式碼 } function applyDestroyStyle(vnode) { ... 更多程式碼 } function applyRemoveStyle(vnode, rm) { ... 更多程式碼 } module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};
snabbdom/modules/eventlisteners.js 程式碼如下:
function updateEventListeners(oldVnode, vnode) { ... 更多程式碼 } module.exports = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners };
如上分析完成各個模組程式碼後,我們再來 看下 snabbdom.js 中的init方法,程式碼如下所示:
/* * @params {modules} 引數值應該是如下了: [ {create: updateClass, update: updateClass}, {create: updateProps, update: updateProps}, {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle}, {create: updateEventListeners, update: updateEventListeners,destroy: updateEventListeners} ] * @params {api} undefined */ var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; function init(modules, api) { var i, j, cbs = {}; if (isUndef(api)) api = domApi; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } } .... 更多程式碼省略 }
因此如上init方法中的 if (isUndef(api)) api = domApi; 因此 api的值就返回了 snabbdom/htmldomapi.js 中的程式碼了。因此api的值變為如下:
api = { createElement: createElement, createElementNS: createElementNS, createTextNode: createTextNode, appendChild: appendChild, removeChild: removeChild, insertBefore: insertBefore, parentNode: parentNode, nextSibling: nextSibling, tagName: tagName, setTextContent: setTextContent };
接著執行下面的for迴圈程式碼:
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; var i, j, cbs = {}; var modules = [ {create: updateClass, update: updateClass}, {create: updateProps, update: updateProps}, {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle}, {create: updateEventListeners, update: updateEventListeners,destroy: updateEventListeners} ]; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } } i = 0 時: cbs = { create: [] } j = 0 時 if (modules[j][hooks[i]] !== undefined) { cbs[hooks[i]].push(modules[j][hooks[i]]); } modules[j][hooks[i]] 的值我們可以理解為: modules[j] = modules[0] = {create: updateClass, update: updateClass}; hooks[i] = hooks[0] 的值為:'create'; 因此 modules[0]['create'] 是有值的。因此 執行if語句內部程式碼,最後cbs值變成如下: cbs = {create: [updateClass]}; 同理 j = 1, j = 2, j = 3 的時候都是一樣的,因此 cbs的值變為如下: cbs = {create: [updateClass, updateProps, updateStyle, updateEventListeners]}; i = 1 時: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [] } 和上面邏輯一樣,同理可知 cbs的值變為如下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners] }; i = 2 時: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [] } 同理可知,最後 cbs值變為如下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle] }; i = 3 時: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [] } 同理可知,最後 cbs值變為如下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners] } i = 4 時: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [] } 同理可知,最後 cbs值變為如下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [] } i = 5 也一樣的,最後cbs的值變為如下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [], post: [] }
最後在 snabbdom.js 中會返回一個函式,基本程式碼如下:
function init(modules, api) { var i, j, cbs = {}; if (isUndef(api)) api = domApi; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } } .... 省略更多程式碼 return function(oldVnode, vnode) { .... 省略更多程式碼 } }
如上程式碼初始化完成後,我們再來看下我們入口js檔案接下來的程式碼,先引入 h 模組;該模組的作用是生成vnode的包裝函式。
/* h 是一個生成vnode的包裝函式 */ var h = require('snabbdom/h');
因此我們再把目光視線再轉移到 snabbdom/h.js中,基本程式碼如下:
var VNode = require('./vnode'); var is = require('./is'); // 新增名稱空間,針對SVG的 function addNS(data, children, sel) { data.ns = 'http://www.w3.org/2000/svg'; if (sel !== 'foreignObject' && children !== undefined) { // 遞迴子節點,新增名稱空間 for (var i = 0; i < children.length; ++i) { addNS(children[i].data, children[i].children, children[i].sel); } } } /* * 把狀態轉為VNode * @param {sel} 選擇器,比如 'div#app' 或 'span' 這樣的等等 * @param {b} 資料 * @param {c} 子節點 * @returns {sel, data, children, text, elm, key} */ module.exports = function h(sel, b, c) { var data = {}, children, text, i; if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } } else if (b !== undefined) { if (is.array(b)) { children = b; } else if (is.primitive(b)) { text = b; } else { data = b; } } if (is.array(children)) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]); } } if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') { addNS(data, children, sel); } return VNode(sel, data, children, text, undefined); };
因此當我們在頁面中如下呼叫程式碼後,它會做哪些事情呢?我們來分析下:
// 構造一個虛擬dom var vnode = h('div#app', {style: {color: '#000'}}, [ h('span', {style: {fontWeight: 'bold'}}, "my name is kongzhi"), ' and xxxx', h('a', {props: {href: '/foo'}}, '我是空智') ] );
把我們的引數傳遞進去走下流程就能明白具體做哪些事情了。
注意:這邊先執行的是先內部的呼叫,然後再依次往外執行呼叫。
因此首先呼叫和執行的程式碼是:
第一步: h('span', {style: {fontWeight: 'bold'}}, "my name is kongzhi"), 因此把引數傳遞進去後:sel: 'span', b = {style: {fontWeight: 'bold'}}, c = "my name is kongzhi";
首先判斷 if (c !== undefined) {} 程式碼,然後進入if語句內部程式碼,如下:
if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } }
因此 data = {style: {fontWeight: 'bold'}}; 然後判斷 c 是否是一個數組,可以看到,不是,因此進入 else if語句,因此 text = "my name is kongzhi"; 從程式碼中可以看到,就直接跳過所有的程式碼了,最後執行 return VNode(sel, data, children, text, undefined); 了,因此會呼叫 snabbdom/vnode.js 程式碼如下:
/* * VNode函式如下:主要的功能是構造VNode, 把輸入的引數轉化為Vnode * @param {sel} 'span' * @param {data} {style: {fontWeight: 'bold'}} * @param {children} undefined * @param {text} "my name is kongzhi" * @param {elm} undefined */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
因此 var key = data.key = undefined; 最後返回值如下:
{ sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }
第二步:呼叫 h('a', {props: {href: '/foo'}}, '我是空智'); 程式碼
同理:sel = 'a'; b = {props: {href: '/foo'}}, c = '我是空智'; 然後執行如下程式碼:
if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } }
因此 data = {props: {href: '/foo'}}; text = '我是空智'; children = undefined; 最後也一樣執行返回:
return VNode(sel, data, children, text, undefined);
因此又呼叫 snabbdom/vnode.js 程式碼如下:
/* * VNode函式如下:主要的功能是構造VNode, 把輸入的引數轉化為Vnode * @param {sel} 'a' * @param {data} {props: {href: '/foo'}} * @param {children} undefined * @param {text} "我是空智" * @param {elm} undefined */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
因此執行程式碼:var key = data.key = undefined; 最後返回值如下:
{ sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined }
第三步呼叫外層的程式碼,把引數傳遞進去,因此程式碼初始化變成如下:
var vnode = h('div#app', {style: {color: '#000'}}, [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, ' and xxxx', { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ] );
繼續把引數傳遞進去,因此 sel = 'div#app'; b = {style: {color: '#000'}}; c 的值變為如下:
c = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, ' and xxxx', { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
首先看if判斷語句,if (c !== undefined) {}; 因此會進入if語句內部程式碼;
if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } }
因此 data = {style: {color: '#000'}}; c 是陣列的話,就把c賦值給children; 因此 children 值為如下:
children = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, ' and xxxx', { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
我們下面接著看 如下程式碼:
if (is.array(children)) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]); } }
如上程式碼,判斷如果 children 是一個數組的話,就迴圈該陣列 children; 從上面我們知道 children 長度為3,因此會迴圈3次。進入for迴圈內部。判斷其中一項是否是數字和字串型別,因此只有 ' and xxxx' 符合要求,因此 children[1] = VNode(undefined, undefined, undefined, ' and xxxx'); 最後會呼叫 snabbdom/vnode.js 程式碼如下:
module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
通過上面的程式碼可知,我們最後返回的是如下:
children[1] = { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined };
執行完成後,我們最後返回程式碼:return VNode(sel, data, children, text, undefined); 因此會繼續呼叫 snabbdom/vnode.js 程式碼如下:
/* @param {sel} 'div#app' @param {data} {style: {color: '#000'}} @param {children} 值變為如下: children = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ]; @param {text} undefined @param {elm} undefined */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
因此繼續執行內部程式碼:var key = undefined; 最後返回程式碼:
return { sel: sel, data: data, children: children, text: text, elm: elm, key: key };
因此最後構造一個虛擬dom返回的值為如下:
vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: undefined, key: undefined }
接著往下執行如下程式碼:
// 初始化容器 var app = document.getElementById('app'); // 將vnode patch 到 app 中 patch(app, vnode);
由於在入口js檔案我們知道patch值為如下:
var patch = snabbdom.init([ require('snabbdom/modules/class'), require('snabbdom/modules/props'), require('snabbdom/modules/style'), require('snabbdom/modules/eventlisteners') ]);
在snabbdom/snabbdom.js 中,如上我們知道,該函式返回了一個函式,程式碼如下:
return function(oldVnode, vnode) { var i, elm, parent; var insertedVnodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); } if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); } for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; };
在這邊我們的引數 oldVnode = 'div#app'; vnode 的值就是我們剛剛返回 vnode 的值。
我們之前分析過我們的cbs返回的值為如下:
cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [], post: [] };
因此我們繼續執行內部程式碼:cbs.pre的長度為0,因此不會執行for迴圈。接著執行如下程式碼:
if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); }
由上面我們知道 oldVnode = 'div#app'; 因此oldVnode.sel = undefined 了;因此進入if語句程式碼內部,即 oldValue = emptyNodeAt(oldVnode); emptyNodeAt 程式碼如下所示:
function emptyNodeAt(elm) { var id = elm.id ? '#' + elm.id : ''; var c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); }
因此 var id = '#app'; 並且判斷該元素elm 是否有類名,如果有類名或多個類名的話,比如有類名為 "xxx yyy" 這樣的,那麼 var c = '.xxx.yyy' 這樣的形式,否則的話 var c = '';
最後返回 return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
由上可知,我們的api的值就返回了 snabbdom/htmldomapi.js 中的程式碼了。值為如下:
api = { createElement: createElement, createElementNS: createElementNS, createTextNode: createTextNode, appendChild: appendChild, removeChild: removeChild, insertBefore: insertBefore, parentNode: parentNode, nextSibling: nextSibling, tagName: tagName, setTextContent: setTextContent };
因此 api.tagName(elm); 會獲取 'div#app' 的tagName, 因此返回 "DIV", 然後使用 .toLowerCase() 方法轉換成小寫,因此 VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); 值變成為 VNode('div' + '#app' + '', {}, [], undefined, "div#app"); 因此變成 VNode('div#app', {}, [], undefined, "div#app"); 這樣的。繼續呼叫 snabbdom/vnode.js 程式碼如下:
/* * @param {sel} 'div#app' * @param {data} {} * @param {children} [] * @param {text} undefined * @param {elm} "div#app" */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; }; var key = undefined;
由上面的引數傳遞進來,因此最後的值返回如下:
return { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined } 因此 oldVnode = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined };
然後我們繼續執行下面的程式碼,如下所示:
if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } }
如上程式碼,sameVnode 函式程式碼在 snobbdom/snobbdom.js 程式碼如下:
function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
判斷vnode1中的key和sel 是否 和 vnode2中的key和sel是否相同,如果相同返回true;說明他們是相同的Vnode. 否則的話,反之。
sel是選擇器的含義。判斷標籤元素上的id和class是否相同。
oldVnode = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined }; vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: undefined, key: undefined }
由上我們可以看到,呼叫 sameVnode(oldVnode, vnode) 方法會返回true。因此只需 patchVnode(oldVnode, vnode, insertedVnodeQueue); 這句程式碼。
var insertedVnodeQueue = [];
patchVnode 函式的作用是判斷 oldVnode 和 newVnode 節點是否相同。該函式程式碼如下:
function isDef(s) { return s !== undefined; } function patchVnode(oldVnode, vnode, insertedVnodeQueue) { var i, hook; if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); } var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; if (oldVnode === vnode) return; if (!sameVnode(oldVnode, vnode)) { var parentElm = api.parentNode(oldVnode.elm); elm = createElm(vnode, insertedVnodeQueue); api.insertBefore(parentElm, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; } if (isDef(vnode.data)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); } if (isDef(hook) && isDef(i = hook.postpatch)) { i(oldVnode, vnode); } }
如上程式碼呼叫patchVnode函式,如上的oldVnode, vnode 值我們上面已經知道了,我們把引數資料傳遞進來,然後 insertedVnodeQueue 為一個空陣列。首先執行如下 if 語句程式碼:
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); }
vnode.data 賦值給i, 因此 i = {style: {color: '#000'}}; 然後使用 isDef 判斷i不等於undefined, 因此返回true。但是 hook = i.hook; 值為undefined,因此isDef(hook = i.hook)值為false,因此最終if語句返回false,if後面的isDef(i = hook.prepatch)語句就不會再去執行了。直接返回false。
程式碼再往下執行:var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children;
因此 var elm = vnode.elm = oldVnode.elm; 即:var elm = vnode.elm = "div#app"; oldCh = oldVnode.children = []; ch = vnode.children 值變為如下:
ch = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
從上面可知:
vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: 'div#app', key: undefined } oldVnode = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined };
接著執行如下程式碼:if (oldVnode === vnode) return; 判斷如果上一次的虛擬節點和新的虛擬節點相同的話,那就不進行頁面渲染操作,直接返回。
繼續執行如下程式碼:
function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } if (!sameVnode(oldVnode, vnode)) { var parentElm = api.parentNode(oldVnode.elm); elm = createElm(vnode, insertedVnodeQueue); api.insertBefore(parentElm, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; }
如上程式碼判斷,如果上一次虛擬節點和新的虛擬節點不相同的話,就執行if語句內部程式碼,如上sameVnode函式程式碼我們也知道,判斷虛擬節點是否相同是通過 虛擬節點中的key和sel屬性來進行判斷的。
sel是選擇器的含義,key是每個標籤中自定義的key。
由於oldValue 和 vnode 上面我們已經知道該值,因此sameVnode(oldVnode, vnode)函式就返回true,最後 !sameVnode(oldVnode, vnode) 就返回false了。說明目前的虛擬節點是相同的。
再接著執行如下程式碼:
function isDef(s) { return s !== undefined; } if (isDef(vnode.data)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); }
由上面可知我們的 vnode.data = {style: {color: '#000'}}; 因此 執行 isDef(vnode.data) 值是不等於undefined的,因此返回true。執行if語句內部程式碼:
由上面分析我們可知 cbs 的值返回如下資料:
cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [], post: [] }; cbs.update = [updateClass, updateProps, updateStyle, updateEventListeners]; 會進入for迴圈。
因此在for迴圈內部。
由上可知 oldValue 和 vnode的值分別為如下:
oldValue = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined }; vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: 'div#app', key: undefined }
i = 0 時;
執行 cbs.update[0](oldVnode, vnode); 程式碼; 就會呼叫 updateClass(oldVnode, vnode) 函式。
updateClass 類在 snabbdom/modules/class.js 內部程式碼如下:
/* 該函式的作用有2點,如下: 1. 從elm中刪除vnode(新虛擬節點)不存在的類名。 2. 將vnode中新增的class新增到elm上去。 */ function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; // 如果舊節點和新節點都沒有class的話,直接返回 if (!oldClass && !klass) return; oldClass = oldClass || {}; klass = klass || {}; /* 如果新虛擬節點中找不到該類名,我們需要從elm中刪除該類名 */ for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } /* 如果新虛擬節點的類名在舊虛擬節點中的類名找不到的話,就新增該類名。 否則的話,舊節點能找到該類名的話,就刪除該類名,也可以理解為: 對html元素不進行重新渲染操作。 */ for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } } } module.exports = {create: updateClass, update: updateClass};
如上程式碼我們可以看到 oldClass = undefined; klass = undefined; 因此不管 i 迴圈多少次,或等於幾,oldClass 和 klass 值都會等於undeinfed的,因此不會執行updateClass內部程式碼的。
i = 1 時;
執行 cbs.update[1](oldValue, vnode); 程式碼,因此會呼叫 updateProps(oldVnode, vnode); 函式。
updateProps 類在 snabbdom/modules/props.js 內部程式碼如下:
/* 如下函式的作用是: 1. 從elm上刪除vnode中不存在的屬性。 2. 更新elm上的屬性。 */ function updateProps(oldVnode, vnode) { var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; // 如果新舊虛擬節點都不存在屬性的話,就直接返回 if (!oldProps && !props) return; oldProps = oldProps || {}; props = props || {}; /* 如果新虛擬節點中沒有該屬性的話,則直接從元素中刪除該屬性。 */ for (key in oldProps) { if (!props[key]) { delete elm[key]; } } // 更新屬性 for (key in props) { cur = props[key]; old = oldProps[key]; /* 如果新舊虛擬節點中屬性不同。且對比的屬性不是value,可以排除 input, textarea這些標籤的value值。及elm上對應的屬性和新虛擬 節點的屬性不相同的話,那麼就需要更新該屬性。 */ if (old !== cur && (key !== 'value' || elm[key] !== cur)) { elm[key] = cur; } } } module.exports = {create: updateProps, update: updateProps};
如上程式碼,我們繼續把oldVnode 和 vnode值傳遞進去,oldValue.data = {}; vnode.data = {style: {color: '#000'}}; 因此 oldProps = oldVnode.data.props = undefined; props = vnode.data.props = undefined; 因此 執行程式碼 if (!oldProps && !props) return; 就執行返回了。
i = 2 時
執行 cbs.update[2](oldValue, vnode); 程式碼,因此會呼叫 updateStyle(oldVnode, vnode); 函式。
updateStyle 類在 snabbdom/modules/style.js, 部分程式碼如下所示:
function updateStyle(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldStyle = oldVnode.data.style, style = vnode.data.style; if (!oldStyle && !style) return; oldStyle = oldStyle || {}; style = style || {}; var oldHasDel = 'delayed' in oldStyle; /* 如果舊虛擬節點有style,新虛擬節點沒有style,因此elm.style[name] 就置空。 */ for (name in oldStyle) { if (!style[name]) { elm.style[name] = ''; } } /* 如果 vnode.data.style 中有 'delayed'的話,則遍歷 style.delayed, 獲取其中一項 cur = style.delayed[name]; 也就是說,如果vnode.style 中的delayed和oldvnode不同的話,則更新delayed的屬性值,並且使用 setNextFrame方法在下一幀將elm的style設定為該值,從而實現動畫過度 效果。 */ for (name in style) { cur = style[name]; if (name === 'delayed') { for (name in style.delayed) { cur = style.delayed[name]; if (!oldHasDel || cur !== oldStyle.delayed[name]) { setNextFrame(elm.style, name, cur); } } } /* 如果 vnode.data.style 中任何項不是remove , 並且不同於oldVnode的 值,則直接設定新值。 */ else if (name !== 'remove' && cur !== oldStyle[name]) { elm.style[name] = cur; } } }
由上我們知道oldVnode 和 vnode的值,因此 oldStyle = oldVnode.data.style; oldVnode.data 值為 {}; 因此 oldStyle = undefined 了; style = vnode.data.style; vnode.data 值為:
vnode.data = {style: {color: '#000'}}; 因此 style = vnode.data.style = {color: '#000'}; 因此程式碼中的if判斷 if (!oldStyle && !style) 返回的false。 程式碼繼續往下執行:
oldStyle = oldStyle || {}; 即 oldStyle = {}; style = style || {}; 即 style = {color: '#000'}; var oldHasDel = 'delayed' in oldStyle; 如果 'delayed' 在 oldStyle 中的話,返回true. 因此這裡返回false, 即 oldHasDel = false; 繼續執行如下for迴圈程式碼:
/* 如果舊虛擬節點有style,新虛擬節點沒有style,因此elm.style[name] 就置空。 */ for (name in oldStyle) { if (!style[name]) { elm.style[name] = ''; } }
由於oldStyle 為 {}; 因此不會進入for迴圈內部,程式碼直接跳過。再接著繼續執行下面的程式碼:
/* 如果 vnode.data.style 中有 'delayed'的話,則遍歷 style.delayed, 獲取其中一項 cur = style.delayed[name]; 也就是說,如果vnode.style 中的delayed和oldvnode不同的話,則更新delayed的屬性值,並且使用setNextFrame方法在下一幀將elm的style設定為該值,從而實現動畫過度效果。 */ for (name in style) { cur = style[name]; if (name === 'delayed') { for (name in style.delayed) { cur = style.delayed[name]; if (!oldHasDel || cur !== oldStyle.delayed[name]) { setNextFrame(elm.style, name, cur); } } } /* 如果 vnode.data.style 中任何項不是remove , 並且不同於oldVnode的 值,則直接設定新值。 */ else if (name !== 'remove' && cur !== oldStyle[name]) { elm.style[name] = cur; } }
由上面分析可知,我們的style值為 {color: '#000'}; 因此遍歷style,該name值不會等於 'delayed'; 但是此時 cur = '#000' 了。因此進入 else if 語句程式碼,並且oldStyle = {}; 因此 cur 肯定不等於 oldStyle[name]; 因此 elm.style[name] = cur; 程式碼就會執行了。也就是說 'div#app' 元素的有樣式 style = "{color: '#000'}" 了。
i = 3 時,
執行 cbs.update[3](oldValue, vnode); 程式碼,因此會呼叫 updateEventListeners(oldVnode, vnode); 函式。
updateEventListeners 類在 snabbdom/modules/eventlisteners.js, 程式碼如下所示:
function invokeHandler(handler, vnode, event) { // ....... } function handleEvent(event, vnode) { var name = event.type, on = vnode.data.on; // call event handler(s) if exists if (on && on[name]) { invokeHandler(on[name], vnode, event); } } function createListener() { return function handler(event) { handleEvent(event, handler.vnode); } } // 上面程式碼是對建立一個事件監聽器邏輯 // 更新事件監聽 function updateEventListeners(oldVnode, vnode) { var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = vnode && vnode.elm, name; // optimization for reused immutable handlers // 如果新舊事件監聽器一樣的話,則直接返回 if (oldOn === on) { return; } // remove existing listeners which no longer used // 如果新節點上沒有事件監聽器,則將舊節點上的事件監聽都刪除 if (oldOn && oldListener) { // if element changed or deleted we remove all existing listeners unconditionally if (!on) { for (name in oldOn) { // remove listener if element was changed or existing listeners removed oldElm.removeEventListener(name, oldListener, false); } } else { /* 否則的話,舊節點的事件監聽器在新節點上事件監聽找不到的話, 則刪除舊節點中的事件監聽器 */ for (name in oldOn) { // remove listener if existing listener removed if (!on[name]) { oldElm.removeEventListener(name, oldListener, false); } } } } // add new listeners which has not already attached if (on) { // reuse existing listener or create new /* 如果oldVnode 上已經有listener的話,則vnode直接使用,否則的話, 新建事件處理器。 */ var listener = vnode.listener = oldVnode.listener || createListener(); // update vnode for listener // 在事件處理器上更新 vnode listener.vnode = vnode; // if element changed or added we add all needed listeners unconditionally // 如果oldVnode上沒有事件處理器的話 if (!oldOn) { /* 且newVnode 是有事件監聽器,因此遍歷,直接將vnode上的事件處理器 新增到elm上。 */ for (name in on) { // add listener if element was changed or new listeners added elm.addEventListener(name, listener, false); } } else { /* 否則的話,如果oldVnode有事件處理器的話,遍歷新 newVnode 節點上 的事件,如果新虛擬節點的事件在 oldVnode 上找不到的話,就把該 事件新增到elm上去。也就是說 oldVnode 上沒有的事件,就新增上去。 */ for (name in on) { // add listener if new listener added if (!oldOn[name]) { elm.addEventListener(name, listener, false); } } } } } module.exports = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners };
如上程式碼呼叫 updateEventListeners(oldVnode, vnode) 函式,為了方便檢視程式碼,我們把oldVnode 和 vnode 值再列印下如下所示:
oldValue = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined }; vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: 'div#app', key: undefined }
因此執行內部程式碼:var oldOn = oldVnode.data.on = undefined; oldListener = oldVnode.listener = undefined; oldElm = oldVnode.elm = 'div#app'; on = vnode && vnode.data.on = undefined;
elm = vnode && vnode.elm = 'div#app'; 然後只需如下if判斷程式碼:
if (oldOn === on) { return; }
如上我們可以看到 oldOn = undefined; on = undefined; 因此程式碼直接返回了。下面的程式碼就不會執行了,說明新舊虛擬節點都沒有監聽器,就不需要更新事件監聽器了。
我們現在把目光視線再回到 snabbdom/snabbdom.js 中的 patchVnode 函式中來,接著執行後面的程式碼如下:
function isDef(s) { return s !== undefined; } i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
因此 i = vnode.data.hook = undefined 了; 因此 下面的if語句直接返回false了,就不會執行 i(oldVnode, vnode); 這個函數了。 現在程式碼繼續往下執行如下程式碼:
function isUndef(s) { return s === undefined; } function isDef(s) { return s !== undefined; } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); }
由上可知,vnode.text = undefined; 因此程式碼 isUndef(vnode.text) 返回true; 執行if語句內部程式碼,由上分析可知:oldCh = oldVnode.children = []; ch值變為如下:
ch = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
因此 oldCh !== ch 為true, 因此會呼叫 updateChildren(elm, oldCh, ch, insertedVnodeQueue); 方法,該方法的程式碼如下所示:
/* @param {parentElm} 'div#app' @param {oldCh} [] @param {newCh} newCh = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ]; @param {insertedVnodeQueue} [] */ function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, elmToMove, before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); 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, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { // New element api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
如上程式碼,我們先看一些初始化的程式碼如下:
var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, elmToMove, before;
如上程式碼可以推斷出 var oldEndIdx = oldCh.length - 1 = -1; var oldStartVnode = oldCh[0] = undefined; var oldEndVnode = oldCh[oldEndIdx] = undefined;
var newEndIdx = newCh.length - 1 = 3 - 1 = 2; var newStartVnode = newCh[0]; 因此 newStartVnode 的值變為如下:
var newStartVnode = { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined };
var newEndVnode = newCh[newEndIdx] = newCh[2]; 因此 newEndVnode 的值變為如下:
newEndVnode = { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined };
因此 我們再整理下如上初始化的值了:
var oldStartIdx = 0; var newStartIdx = 0; var oldEndIdx = -1; var oldStartVnode = undefined; var oldEndVnode = undefined; var newEndIdx = 2; var newStartVnode = { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }; var newEndVnode = { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined }; var oldKeyToIdx, idxInOld, elmToMove, before;
接下來執行 while 迴圈語句:
/* 由上分析可知: oldStartIdx = 0; oldEndIdx = -1; newStartIdx = 0; newEndIdx = 2; */ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { }
因此while迴圈語句返回的是false,不會進入內部程式碼進行判斷。繼續執行如下程式碼:
function isUndef(s) { return s === undefined; } if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); }
由上分析可知:oldStartIdx = 0; oldEndIdx = -1; 因此會進入if語句程式碼:執行 before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm;
首先程式碼 newCh[newEndIdx+1] = newCh[2+1] = newCh[3] = undefined; 因此 isUndef(newCh[newEndIdx+1]) 程式碼為true; 因此此時 before = null; 接著程式碼往下執行:
/* @param {parentElm} 'div#app' @param {before} null @param {newCh} newCh = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ]; @param {newStartIdx} 0 @param {newEndIdx} 2 @param {insertedVnodeQueue} [] */ addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); 函式。 function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before); } }
引數傳遞進來後,因此形參各個值分別對應如下:
parentElm = 'div#app'; before = null; vnodes = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: unde