zoukankan      html  css  js  c++  java
  • vue系列---snabbdom.js使用及源码分析(九)

    一:什么是snabbdom?

    在学习Vue或React中,我们了解最多的就是虚拟DOM,虚拟DOM可以看作是一颗模拟了DOM的Javascript树,主要是通过vnode实现一个无状态的组件,当组件状态发生变更时,就会触发 virtual-dom 数据的变化,然后使用虚拟节点树进行渲染,但是在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比,只渲染两者之间不同的部分。

    为什么我们需要虚拟DOM呢?

    在web很早时期,我们使用jquery来做页面的交互,比如如下排序这么一个demo。代码如下:

    <!DOCTYPE html>
    <html>
    <head>
      <title></title>
      <meta charset="utf-8">
      <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
    </head>
    <body>
      <div id="app">
      </div>
      <div id="sort" style="margin-top: 20px;">按年纪排序</div>
      <script type="text/javascript">
        var datas = [
          { 'name': 'kongzhi11', 'age': 32 },
          { 'name': 'kongzhi44', 'age': 29 },
          { 'name': 'kongzhi22', 'age': 31 },
          { 'name': 'kongzhi33', 'age': 30 }
        ];
        var render = function() {
          var html = '';
          datas.forEach(function(item, index) {
            html += `<li>
                      <div class="u-cls">
                        <span class="name">姓名:${item.name}</span>
                        <span class="age" style="margin-left:20px;">年龄:${item.age}</span>
                        <span class="closed">x</span>
                      </div>
                    </li>`;
          });
          return html;
        };
        $("#app").html(render());
        $('#sort').on('click', function() {
          datas = datas.sort(function(a, b) {
            return a.age - b.age;
          });
          $('#app').html(render());
        })
      </script>
    </body>
    </html>

    如上demo排序,虽然在使用jquery时代这种方式是可行的,我们点击按钮,它就可以从小到大的排序,但是它比较暴力,它会将之前的dom全部删除,然后重新渲染新的dom节点,我们知道,操作DOM会影响页面的性能,并且有时候数据根本就没有发生改变,我们希望未更改的数据不需要重新渲染操作。因此虚拟DOM的思想就出来了,虚拟DOM的思想是先控制数据再到视图,但是数据状态是通过diff比对,它会比对新旧虚拟DOM节点,然后找出两者之前的不同,然后再把不同的节点再发生渲染操作。

    如下图演示:

    snabbdom 是虚拟DOM的一种简单的实现,并且在Vue中实现的虚拟DOM是借鉴了 snabbdom.js 的,因此我们这边首先来学习该库。

    如果我们自己需要实现一个虚拟DOM,我们一般有如下三个步骤需要完成:

    1. compile, 我们如何能把真实的DOM编译成Vnode。
    2. diff. 我们怎么样知道oldVnode和newVnode之间的不同。
    3. patch. 通过第二步的比较知道不同点,然后把不同的虚拟DOM渲染到真实的DOM上去。

    snabbdom库我们可以到github源码下载一份,github地址为:https://github.com/snabbdom/snabbdom/tree/8079ba78685b0f0e0e67891782c3e8fb9d54d5b8,我这边下载的是0.5.4版本的,因为从v0.6.0版本之上使用的是 typescript编写的,对于没有使用过typescript人来说,理解起来可能并不那么顺利,因此我这边就来分析下使用javascript编写的代码。
    对于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 库对新旧虚拟节点进行对比,然后找出不同的节点来,然后对不同的节点进行渲染的整个分析过程。

  • 相关阅读:
    劳动节CF题总结
    「联合省选 2020 A」作业题 做题心得
    bzoj3784 树上的路径
    [AGC039E] Pairing Points
    [AGC012E] Camel and Oases
    [AGC011F] Train Service Planning
    [AGC039F] Min Product Sum
    Pedersen commitment原理
    标准模型(standard model)与随机语言模型(random oracle model)
    会议论文引用缩写标准 PDF
  • 原文地址:https://www.cnblogs.com/tugenhua0707/p/11762585.html
Copyright © 2011-2022 走看看