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

一:什麼是snabbdom?javascript

在學習Vue或React中,咱們瞭解最多的就是虛擬DOM,虛擬DOM能夠看做是一顆模擬了DOM的Javascript樹,主要是經過vnode實現一個無狀態的組件,當組件狀態發生變動時,就會觸發 virtual-dom 數據的變化,而後使用虛擬節點樹進行渲染,可是在渲染以前,會使用新生成的虛擬節點樹和上一次生成的虛擬節點樹進行對比,只渲染二者之間不一樣的部分。html

爲何咱們須要虛擬DOM呢?前端

在web很早時期,咱們使用jquery來作頁面的交互,好比以下排序這麼一個demo。代碼以下:java

<!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節點,而後找出二者以前的不一樣,而後再把不一樣的節點再發生渲染操做。node

以下圖演示:jquery

snabbdom 是虛擬DOM的一種簡單的實現,而且在Vue中實現的虛擬DOM是借鑑了 snabbdom.js 的,所以咱們這邊首先來學習該庫。webpack

若是咱們本身須要實現一個虛擬DOM,咱們通常有以下三個步驟須要完成:git

1. compile, 咱們如何能把真實的DOM編譯成Vnode。
2. diff. 咱們怎麼樣知道oldVnode和newVnode之間的不一樣。
3. patch. 經過第二步的比較知道不一樣點,而後把不一樣的虛擬DOM渲染到真實的DOM上去。github

snabbdom庫咱們能夠到github源碼下載一份,github地址爲:https://github.com/snabbdom/snabbdom/tree/8079ba78685b0f0e0e67891782c3e8fb9d54d5b8,我這邊下載的是0.5.4版本的,由於從v0.6.0版本之上使用的是 typescript編寫的,對於沒有使用過typescript人來講,理解起來可能並不那麼順利,所以我這邊就來分析下使用javascript編寫的代碼。
對於javascript來講,前端開發人員仍是熟悉的,因此分析了下0.5.4版本的內部原理。web

注意:無論新版本仍是前一個版本,內部的基本原理是相似的。新增的版本可能會新增一些新功能。可是不影響咱們理解主要的功能。

咱們從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 主要的接口有:

一、 h(type, data, children),返回 Virtual DOM 樹。

二、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 庫對新舊虛擬節點進行對比,而後找出不一樣的節點來,而後對不一樣的節點進行渲染的整個分析過程。

相關文章
相關標籤/搜索