vue系列—snabbdom.js使用及源碼分析(九)

  • 2019 年 10 月 30 日
  • 筆記

一:什麼是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編寫的代碼。
對於javascript來說,前端開發人員還是熟悉的,所以分析了下0.5.4版本的內部原理。

注意:不管新版本還是前一個版本,內部的基本原理是類似的。新增的版本可能會新增一些新功能。但是不影響我們理解主要的功能。

我們從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 = 0if (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: undefined,      text: ' and xxxx',      elm: undefined,      key: undefined    },    {      sel: 'a',      data: {props: {href: '/foo'}},      children: undefined,      text: "我是空智",      elm: undefined,      key: undefined    }  ];  startIdx = 0;  endIdx = 2;  insertedVnodeQueue = [];

由上可知,我們的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  };

addVnodes 函數執行內部for循環代碼如下:

for (; startIdx <= endIdx; ++startIdx) {    api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);  }  // newNode 節點插入到 referenceNode 前面去  function insertBefore(parentNode, newNode, referenceNode){    parentNode.insertBefore(newNode, referenceNode);  }  // 該函數的代碼在 snabbdom/snabbdom.js 中  function createElm(vnode, insertedVnodeQueue) {    var i, data = vnode.data;    if (isDef(data)) {      if (isDef(i = data.hook) && isDef(i = i.init)) {        i(vnode);        data = vnode.data;      }    }    var elm, children = vnode.children, sel = vnode.sel;    if (isDef(sel)) {      // Parse selector      var hashIdx = sel.indexOf('#');      var dotIdx = sel.indexOf('.', hashIdx);      var hash = hashIdx > 0 ? hashIdx : sel.length;      var dot = dotIdx > 0 ? dotIdx : sel.length;      var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;      elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag)                                                          : api.createElement(tag);      if (hash < dot) elm.id = sel.slice(hash + 1, dot);      if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/./g, ' ');      if (is.array(children)) {        for (i = 0; i < children.length; ++i) {          api.appendChild(elm, createElm(children[i], insertedVnodeQueue));        }      } else if (is.primitive(vnode.text)) {        api.appendChild(elm, api.createTextNode(vnode.text));      }      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);      i = vnode.data.hook; // Reuse variable      if (isDef(i)) {        if (i.create) i.create(emptyNode, vnode);        if (i.insert) insertedVnodeQueue.push(vnode);      }    } else {      elm = vnode.elm = api.createTextNode(vnode.text);    }    return vnode.elm;  }

因此 startIdx = 0 的時候:

api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);

parentElm 為 ‘div#app’ 節點;  vnodes[startIdx] = vnodes[0];

vnodes[0] = {    sel: 'span',    data: {style: {fontWeight: 'bold'}},    children: undefined,    text: "my name is kongzhi",    elm: undefined,    key: undefined  }

由上面分析:before = null;

因此代碼 api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before); 的含義是:往父節點 ‘div#app’ 子元素上之前插入 vnodes[0] 這個節點進去。insertedVnodeQueue 此時為[]; 現在我們再來看看 createElm 函數代碼吧。

/*   @param {vnode}    vnode = {      sel: 'span',      data: {style: {fontWeight: 'bold'}},      children: undefined,      text: "my name is kongzhi",      elm: undefined,      key: undefined    }    @param {insertedVnodeQueue} []   */  function createElm(vnode, insertedVnodeQueue) {    var i, data = vnode.data;    if (isDef(data)) {      if (isDef(i = data.hook) && isDef(i = i.init)) {        i(vnode);        data = vnode.data;      }    }    var elm, children = vnode.children, sel = vnode.sel;    ..... 更多代碼  }  function isDef(s) { return s !== undefined; }

由上代碼:var data = vnode.data = {style: {fontWeight: ‘bold’}}; if (isDef(data)) {} if判斷語句,判斷data不等於undefined; 因此返回true。繼續執行內部代碼:

if (isDef(i = data.hook) && isDef(i = i.init)) {    i(vnode);    data = vnode.data;  }

data.hook = undefined; 因此 if語句返回false; 此時跳過代碼; 代碼繼續往下執行:

var children = vnode.children = undefined; var sel = vnode.sel = 'span';

代碼繼續往下執行; 如下代碼:

if (isDef(sel)) {    // Parse selector    var hashIdx = sel.indexOf('#');    var dotIdx = sel.indexOf('.', hashIdx);    var hash = hashIdx > 0 ? hashIdx : sel.length;    var dot = dotIdx > 0 ? dotIdx : sel.length;    var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;    elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag)                                                        : api.createElement(tag);    if (hash < dot) elm.id = sel.slice(hash + 1, dot);    if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/./g, ' ');    if (is.array(children)) {      for (i = 0; i < children.length; ++i) {        api.appendChild(elm, createElm(children[i], insertedVnodeQueue));      }    } else if (is.primitive(vnode.text)) {      api.appendChild(elm, api.createTextNode(vnode.text));    }    for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);    i = vnode.data.hook; // Reuse variable    if (isDef(i)) {      if (i.create) i.create(emptyNode, vnode);      if (i.insert) insertedVnodeQueue.push(vnode);    }  } else {    elm = vnode.elm = api.createTextNode(vnode.text);  }  return vnode.elm;

如上代碼; 判斷 sel 不等於 undefined; 此時sel為 ‘span’; 因此if語句返回true。繼續進入if內部代碼: var hashIdx = sel.indexOf(‘#’) = -1; 該代碼的含義是判斷 sel選擇器是否為 id選擇器。
如果為 -1; 說明不是id選擇器。
var dotIdx = sel.indexOf(‘.’, hashIdx); 這裡代碼的含義判斷sel選擇器是否為 “類名” 選擇器,如果返回 -1; 說明也不是 class 選擇器。
因此 var dotIdx = -1; var hash = hashIdx > 0 ? hashIdx : sel.length; 即 hash = sel.length = ‘span’.length = 4; var dot = dotIdx > 0 ? dotIdx : sel.length; var dot = sel.length = 4;
var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; 因此 tag = sel = ‘span’;
elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag) : api.createElement(tag); createElementNS() 方法可創建帶有指定命名空間的元素節點。
此方法可返回一個 Element 對象。因此 isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag) 代碼的含義是:如果data不等於undefined; 且 data.ns 也不等於undefined的話,就使用 createElementNS 方法創建帶有指定命名空間的元素節點。那麼在這裡 data.ns 為 undefined; 因此 elm = api.createElement(tag); 也就是說 elm = document.createElement(‘span’) 這樣的,動態創建一個span標籤元素。

接着代碼往下執行,if (hash < dot) elm.id = sel.slice(hash + 1, dot); 由上可知:hash = 4; dot = 4; 因此代碼跳過。

if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/./g, ‘ ‘); 由上可知:dotIdx = -1; 如果 dotIdx 大於0的話,說明他是類名選擇器,也就是說 ‘span’ 標籤帶有class類名,如果帶有class類名的話,比如為 ‘span.xx.yy’; 因此 sel = ‘span.xx.yy’; 因此就會執行 elm.className = sel.slice(hash + 1, dot).replace(/./g, ‘ ‘); 也就是說 把 span.xx.yy 對應的類名 xx yy取出來放入到 elm.className 中。因此可以理解為 變成這樣的 ‘<div id=”app” class=”xx yy”></div>’ 的代碼。

再接着執行如下代碼:

if (is.array(children)) {    for (i = 0; i < children.length; ++i) {      api.appendChild(elm, createElm(children[i], insertedVnodeQueue));    }  } else if (is.primitive(vnode.text)) {    api.appendChild(elm, api.createTextNode(vnode.text));  }

如上代碼判斷 children 是否為一個數組,如果是數組的話,就循環該數組,然後把該數組的某一項插入到elm子元素中的後面去。在代碼這裡我們的children為undefined。因此會進入 else if 語句代碼判斷, else if (is.primitive(vnode.text)) { }; vnode.text 的值為 = “my name is kongzhi”; 因此 is.primitive(vnode.text) 返回true。因此會創建一個 “my name is kongzhi” 的文本節點插入到elm後面去。在這裡elm為 ‘span’ 元素,因此就會變成 “<span>my name is kongzhi</span>” 這樣的html元素了。

繼續執行如下代碼:

for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);  i = vnode.data.hook; // Reuse variable

由上分析可知,我們的cbs的值為如下:

cbs = {    create: [updateClass, updateProps, updateStyle, updateEventListeners],    update: [updateClass, updateProps, updateStyle, updateEventListeners],    remove: [applyRemoveStyle],    destroy: [applyDestroyStyle, updateEventListeners],    pre: [],    post: []  };

emptyNode 的值在 snabbdom/snabbdom.js 中的頂部定義為如下代碼:

var emptyNode = VNode('', {}, [], undefined, undefined);

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};  };

因此最後 emptyNode = {    sel: '',    data: {},    children: [],    text: undefined,    elm: undefined,    key: undefined  };

把我們的目標視線放到上面的for循環中,看看代碼是如何執行的,如代碼: for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);

cbs.create = [updateClass, updateProps, updateStyle, updateEventListeners];

因此for循環會循環四次。因此 當 i = 0 的時候,就會調用 snabbdom/modules/class.js 中updateClass函數,目的是更新類名,當 i = 1 的時候,就會調用 snabbdom/modules/props.js 中的updateProps函數,目的是更新元素中的屬性。當 i = 2 的時候,會調用 snabbdom/modules/style.js 中的updateStyle函數,該函數的作用是更新元素中的 style 樣式。當 i = 3的時候,就會調用 snabbdom/modules/eventlisteners.js 中的 updateEventListeners的函數,該函數的作用是更新元素上的事件監聽器。

如上分析,我們來簡單的走下流程,看看最終會變成什麼樣的一個過程。

i = 0 時;

執行代碼:cbs.create[0](emptyNode, vnode); 函數。因此調用 snabbdom/modules/class.js 中updateClass函數。updateClass函數代碼如下:

/*     @param {oldVnode}     oldVnode = {        sel: '',        data: {},        children: [],        text: undefined,        elm: undefined,        key: undefined     };     @param {vnode}     vnode = {      sel: 'span',      data: {style: {fontWeight: 'bold'}},      children: undefined,      text: "my name is kongzhi",      elm: undefined,      key: undefined    }     */      /*     該函數的作用有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);        }      }    }

上面代碼執行後,初始化參數值分別為如下值:

var cur, name, elm = vnode.elm = undefined;  var oldClass = oldVnode.data.class = undefined;  var klass = vnode.data.class = undefined;    if (!oldClass && !klass) return; 直接返回。

i = 1 時;

執行代碼:cbs.create[1](emptyNode, vnode); 函數。因此調用 snabbdom/modules/props.js 中updateProps函數。updateProps函數代碼如下:

/*     @param {oldVnode}     oldVnode = {        sel: '',        data: {},        children: [],        text: undefined,        elm: undefined,        key: undefined     };     @param {vnode}     vnode = {       sel: 'span',       data: {style: {fontWeight: 'bold'}},       children: undefined,       text: "my name is kongzhi",       elm: undefined,       key: undefined     }    */    /*      如下函數的作用是:      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;        }      }    }

如上代碼繼續初始化如下:

var key, cur, old, elm = vnode.elm = undefined,        oldProps = oldVnode.data.props = undefined, props = vnode.data.props = undefined;    // 如果新舊虛擬節點都不存在屬性的話,就直接返回    if (!oldProps && !props) return;

也直接返回函數。

i = 2 時,

執行代碼:cbs.create[2](emptyNode, vnode); 函數。會調用 snabbdom/modules/style.js 中的updateStyle函數; updateStyle函數代碼如下:

/*     @param {oldVnode}     oldVnode = {        sel: '',        data: {},        children: [],        text: undefined,        elm: undefined,        key: undefined     };     @param {vnode}     vnode = {       sel: 'span',       data: {style: {fontWeight: 'bold'}},       children: undefined,       text: "my name is kongzhi",       elm: undefined,       key: undefined     }    */    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;        }      }    }

繼續初始化參數代碼如下:

var cur, name, elm = vnode.elm = undefined,        oldStyle = oldVnode.data.style = undefined,        style = vnode.data.style = {fontWeight: 'bold'};      if (!oldStyle && !style) return;

如上style有值,因此 !style 返回false,繼續執行後面的代碼如下:

oldStyle = oldStyle || {};  style = style || {};  var oldHasDel = 'delayed' in oldStyle = false;

如上代碼可知:style = {fontWeight: ‘bold’};
繼續執行如下代碼:

/*      如果舊虛擬節點有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;      }    }

如上可知:oldStyle = undefined; 因此跳過 for (name in oldStyle) {} 循環。繼續下面的for循環操作。

for (name in style) {      cur = style[name];      // 下面的if代碼內部是不會執行的。      if (name === 'delayed') {}  }

如上代碼可知:style = {fontWeight: ‘bold’}; 因此 cur = ‘bold’; 由於 name = “fontWeight”; 因此不會進入 if (name === ‘delayed’) {} if語句代碼內部。跳到else if 代碼執行:

/*     如果 vnode.data.style 中任何項不是remove , 並且不同於oldVnode的     值,則直接設置新值。     */    else if (name !== 'remove' && cur !== oldStyle[name]) {      elm.style[name] = cur;    }

因此最後 會執行 elm.style[name] = cur; 也就是說 html 元素代碼被渲染成 “<span style=”fontWeight: ‘bold'”></span>”; 這個樣子。

i = 3的時

就會調用 snabbdom/modules/eventlisteners.js 中的 updateEventListeners的函數,該函數的作用是更新元素上的事件監聽器。

該函數代碼如下:    /*     @param {oldVnode}     oldVnode = {        sel: '',        data: {},        children: [],        text: undefined,        elm: undefined,        key: undefined     };     @param {vnode}     vnode = {       sel: 'span',       data: {style: {fontWeight: 'bold'}},       children: undefined,       text: "my name is kongzhi",       elm: undefined,       key: undefined     }    */    // 更新事件監聽    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);            }          }        }      }    }

繼續初始化參數如下:

var oldOn = oldVnode.data.on = undefined,        oldListener = oldVnode.listener = undefined,        oldElm = oldVnode.elm = undefined,        on = vnode && vnode.data.on = undefined,        elm = vnode && vnode.elm = undefined,        name;      // 如果新舊事件監聽器一樣的話,則直接返回    if (oldOn === on) {      return;    }

因此不管舊節點也好,還是新節點也好,都沒有事件監聽器,那麼就直接返回,不做任何事情。

執行完成後,我們可以看到 在渲染dom元素的時候,我們會比較新舊虛擬節點之間的不同,然後把不同的class(類名), props(屬性), style(樣式)及 eventListener(事件)分別會重新渲染。

我們再回到 function createElm(vnode, insertedVnodeQueue) {} 函數內部再執行下面的代碼;

i = vnode.data.hook; 即:i = undefined;      function isDef(s) { return s !== undefined; }      if (isDef(i)) {      if (i.create) i.create(emptyNode, vnode);      if (i.insert) insertedVnodeQueue.push(vnode);    }

再執行如上代碼;我們知道 i = undefined; 因此 isDef(i) = false; 跳過內部代碼; 繼續往下執行。

因此最後就返回 return vnode.elm; 這句代碼; 也就是返回 “<span style=”font-weight:bold;”>my name is kongzhi</span>”。

因此就會把該span標籤元素插入到 ‘div#app’ 子元素的前面去。

我們繼續看如下代碼:

for (; startIdx <= endIdx; ++startIdx) {      api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);    }

如上是 startIdx = 0 的情況下,endIdx = 2; 因此當 startIdx = 1; 和 startIdx = 2 的情況下; 也會把 新舊不同的虛擬節點渲染到html元素上去的,因此會把vnodes節點都渲染上去; 如下vnodes的值:

vnodes = [      {        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 patch 到 app 中    patch(app, vnode);

就會把html元素渲染成如下這個樣子:

`<div id="app">      <span style="font-weight:bold;">my name is kongzhi</span>      and xxx      <a href="/foo">我是空智</a>    </div>`;

同理回到我們的入口文件中的代碼來。如下代碼:

// 創建一個新的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);

如上我們創建一個新的vnode的時候,他會生成一個新的虛擬節點 newVnode, 該新的虛擬節點會與舊的虛擬節點進行對比,然後執行過程和上面的一樣,分別會對元素的 屬性、樣式、文本節點、事件監聽器、類名 或 id 進行對比,找出不同的節點,然後又會使用 createElm 函數進行分別渲染出來,因此最後我們的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>

如上就是 snabbdom.js 庫對新舊虛擬節點進行對比,然後找出不同的節點來,然後對不同的節點進行渲染的整個分析過程。