一:什么是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 = 0 时 if (modules[j][hooks[i]] !== undefined) { cbs[hooks[i]].push(modules[j][hooks[i]]); } modules[j][hooks[i]] 的值咱们能够理解为: modules[j] = modules[0] = {create: updateClass, update: updateClass}; hooks[i] = hooks[0] 的值为:'create'; 所以 modules[0]['create'] 是有值的。所以 执行if语句内部代码,最后cbs值变成以下: cbs = {create: [updateClass]}; 同理 j = 1, j = 2, j = 3 的时候都是同样的,所以 cbs的值变为以下: cbs = {create: [updateClass, updateProps, updateStyle, updateEventListeners]}; i = 1 时: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [] } 和上面逻辑同样,同理可知 cbs的值变为以下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners] }; i = 2 时: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [] } 同理可知,最后 cbs值变为以下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle] }; i = 3 时: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [] } 同理可知,最后 cbs值变为以下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners] } i = 4 时: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [] } 同理可知,最后 cbs值变为以下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [] } i = 5 也同样的,最后cbs的值变为以下: cbs = { create: [updateClass, updateProps, updateStyle, updateEventListeners], update: [updateClass, updateProps, updateStyle, updateEventListeners], remove: [applyRemoveStyle], destroy: [applyDestroyStyle, updateEventListeners], pre: [], post: [] }
最后在 snabbdom.js 中会返回一个函数,基本代码以下:
function init(modules, api) { var i, j, cbs = {}; if (isUndef(api)) api = domApi; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]); } } .... 省略更多代码 return function(oldVnode, vnode) { .... 省略更多代码 } }
如上代码初始化完成后,咱们再来看下咱们入口js文件接下来的代码,先引入 h 模块;该模块的做用是生成vnode的包装函数。
/* h 是一个生成vnode的包装函数 */ var h = require('snabbdom/h');
所以咱们再把目光视线再转移到 snabbdom/h.js中,基本代码以下:
var VNode = require('./vnode'); var is = require('./is'); // 添加命名空间,针对SVG的 function addNS(data, children, sel) { data.ns = 'http://www.w3.org/2000/svg'; if (sel !== 'foreignObject' && children !== undefined) { // 递归子节点,添加命名空间 for (var i = 0; i < children.length; ++i) { addNS(children[i].data, children[i].children, children[i].sel); } } } /* * 把状态转为VNode * @param {sel} 选择器,好比 'div#app' 或 'span' 这样的等等 * @param {b} 数据 * @param {c} 子节点 * @returns {sel, data, children, text, elm, key} */ module.exports = function h(sel, b, c) { var data = {}, children, text, i; if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } } else if (b !== undefined) { if (is.array(b)) { children = b; } else if (is.primitive(b)) { text = b; } else { data = b; } } if (is.array(children)) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]); } } if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g') { addNS(data, children, sel); } return VNode(sel, data, children, text, undefined); };
所以当咱们在页面中以下调用代码后,它会作哪些事情呢?咱们来分析下:
// 构造一个虚拟dom var vnode = h('div#app', {style: {color: '#000'}}, [ h('span', {style: {fontWeight: 'bold'}}, "my name is kongzhi"), ' and xxxx', h('a', {props: {href: '/foo'}}, '我是空智') ] );
把咱们的参数传递进去走下流程就能明白具体作哪些事情了。
注意:这边先执行的是先内部的调用,而后再依次往外执行调用。
所以首先调用和执行的代码是:
第一步: h('span', {style: {fontWeight: 'bold'}}, "my name is kongzhi"), 所以把参数传递进去后:sel: 'span', b = {style: {fontWeight: 'bold'}}, c = "my name is kongzhi";
首先判断 if (c !== undefined) {} 代码,而后进入if语句内部代码,以下:
if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } }
所以 data = {style: {fontWeight: 'bold'}}; 而后判断 c 是不是一个数组,能够看到,不是,所以进入 else if语句,所以 text = "my name is kongzhi"; 从代码中能够看到,就直接跳过全部的代码了,最后执行 return VNode(sel, data, children, text, undefined); 了,所以会调用 snabbdom/vnode.js 代码以下:
/* * VNode函数以下:主要的功能是构造VNode, 把输入的参数转化为Vnode * @param {sel} 'span' * @param {data} {style: {fontWeight: 'bold'}} * @param {children} undefined * @param {text} "my name is kongzhi" * @param {elm} undefined */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
所以 var key = data.key = undefined; 最后返回值以下:
{ sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }
第二步:调用 h('a', {props: {href: '/foo'}}, '我是空智'); 代码
同理:sel = 'a'; b = {props: {href: '/foo'}}, c = '我是空智'; 而后执行以下代码:
if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } }
所以 data = {props: {href: '/foo'}}; text = '我是空智'; children = undefined; 最后也同样执行返回:
return VNode(sel, data, children, text, undefined);
所以又调用 snabbdom/vnode.js 代码以下:
/* * VNode函数以下:主要的功能是构造VNode, 把输入的参数转化为Vnode * @param {sel} 'a' * @param {data} {props: {href: '/foo'}} * @param {children} undefined * @param {text} "我是空智" * @param {elm} undefined */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
所以执行代码:var key = data.key = undefined; 最后返回值以下:
{ sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined }
第三步调用外层的代码,把参数传递进去,所以代码初始化变成以下:
var vnode = h('div#app', {style: {color: '#000'}}, [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, ' and xxxx', { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ] );
继续把参数传递进去,所以 sel = 'div#app'; b = {style: {color: '#000'}}; c 的值变为以下:
c = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, ' and xxxx', { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
首先看if判断语句,if (c !== undefined) {}; 所以会进入if语句内部代码;
if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } }
所以 data = {style: {color: '#000'}}; c 是数组的话,就把c赋值给children; 所以 children 值为以下:
children = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, ' and xxxx', { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
咱们下面接着看 以下代码:
if (is.array(children)) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]); } }
如上代码,判断若是 children 是一个数组的话,就循环该数组 children; 从上面咱们知道 children 长度为3,所以会循环3次。进入for循环内部。判断其中一项是不是数字和字符串类型,所以只有 ' and xxxx' 符合要求,所以 children[1] = VNode(undefined, undefined, undefined, ' and xxxx'); 最后会调用 snabbdom/vnode.js 代码以下:
module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
经过上面的代码可知,咱们最后返回的是以下:
children[1] = { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined };
执行完成后,咱们最后返回代码:return VNode(sel, data, children, text, undefined); 所以会继续调用 snabbdom/vnode.js 代码以下:
/* @param {sel} 'div#app' @param {data} {style: {color: '#000'}} @param {children} 值变为以下: children = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ]; @param {text} undefined @param {elm} undefined */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; };
所以继续执行内部代码:var key = undefined; 最后返回代码:
return { sel: sel, data: data, children: children, text: text, elm: elm, key: key };
所以最后构造一个虚拟dom返回的值为以下:
vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: undefined, key: undefined }
接着往下执行以下代码:
// 初始化容器 var app = document.getElementById('app'); // 将vnode patch 到 app 中 patch(app, vnode);
因为在入口js文件咱们知道patch值为以下:
var patch = snabbdom.init([ require('snabbdom/modules/class'), require('snabbdom/modules/props'), require('snabbdom/modules/style'), require('snabbdom/modules/eventlisteners') ]);
在snabbdom/snabbdom.js 中,如上咱们知道,该函数返回了一个函数,代码以下:
return function(oldVnode, vnode) { var i, elm, parent; var insertedVnodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); } if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]); } for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; };
在这边咱们的参数 oldVnode = 'div#app'; vnode 的值就是咱们刚刚返回 vnode 的值。
咱们以前分析过咱们的cbs返回的值为以下:
cbs = {
create: [updateClass, updateProps, updateStyle, updateEventListeners],
update: [updateClass, updateProps, updateStyle, updateEventListeners],
remove: [applyRemoveStyle],
destroy: [applyDestroyStyle, updateEventListeners],
pre: [],
post: []
};
所以咱们继续执行内部代码:cbs.pre的长度为0,所以不会执行for循环。接着执行以下代码:
if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); }
由上面咱们知道 oldVnode = 'div#app'; 所以oldVnode.sel = undefined 了;所以进入if语句代码内部,即 oldValue = emptyNodeAt(oldVnode); emptyNodeAt 代码以下所示:
function emptyNodeAt(elm) { var id = elm.id ? '#' + elm.id : ''; var c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); }
所以 var id = '#app'; 而且判断该元素elm 是否有类名,若是有类名或多个类名的话,好比有类名为 "xxx yyy" 这样的,那么 var c = '.xxx.yyy' 这样的形式,不然的话 var c = '';
最后返回 return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
由上可知,咱们的api的值就返回了 snabbdom/htmldomapi.js 中的代码了。值为以下:
api = {
createElement: createElement,
createElementNS: createElementNS,
createTextNode: createTextNode,
appendChild: appendChild,
removeChild: removeChild,
insertBefore: insertBefore,
parentNode: parentNode,
nextSibling: nextSibling,
tagName: tagName,
setTextContent: setTextContent
};
所以 api.tagName(elm); 会获取 'div#app' 的tagName, 所以返回 "DIV", 而后使用 .toLowerCase() 方法转换成小写,所以 VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); 值变成为 VNode('div' + '#app' + '', {}, [], undefined, "div#app"); 所以变成 VNode('div#app', {}, [], undefined, "div#app"); 这样的。继续调用 snabbdom/vnode.js 代码以下:
/* * @param {sel} 'div#app' * @param {data} {} * @param {children} [] * @param {text} undefined * @param {elm} "div#app" */ module.exports = function(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; }; var key = undefined;
由上面的参数传递进来,所以最后的值返回以下:
return { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined } 所以 oldVnode = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined };
而后咱们继续执行下面的代码,以下所示:
if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } }
如上代码,sameVnode 函数代码在 snobbdom/snobbdom.js 代码以下:
function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
判断vnode1中的key和sel 是否 和 vnode2中的key和sel是否相同,若是相同返回true;说明他们是相同的Vnode. 不然的话,反之。
sel是选择器的含义。判断标签元素上的id和class是否相同。
oldVnode = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined }; vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: undefined, key: undefined }
由上咱们能够看到,调用 sameVnode(oldVnode, vnode) 方法会返回true。所以只需 patchVnode(oldVnode, vnode, insertedVnodeQueue); 这句代码。
var insertedVnodeQueue = [];
patchVnode 函数的做用是判断 oldVnode 和 newVnode 节点是否相同。该函数代码以下:
function isDef(s) { return s !== undefined; } function patchVnode(oldVnode, vnode, insertedVnodeQueue) { var i, hook; if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); } var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; if (oldVnode === vnode) return; if (!sameVnode(oldVnode, vnode)) { var parentElm = api.parentNode(oldVnode.elm); elm = createElm(vnode, insertedVnodeQueue); api.insertBefore(parentElm, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; } if (isDef(vnode.data)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); } if (isDef(hook) && isDef(i = hook.postpatch)) { i(oldVnode, vnode); } }
如上代码调用patchVnode函数,如上的oldVnode, vnode 值咱们上面已经知道了,咱们把参数数据传递进来,而后 insertedVnodeQueue 为一个空数组。首先执行以下 if 语句代码:
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); }
vnode.data 赋值给i, 所以 i = {style: {color: '#000'}}; 而后使用 isDef 判断i不等于undefined, 所以返回true。可是 hook = i.hook; 值为undefined,所以isDef(hook = i.hook)值为false,所以最终if语句返回false,if后面的isDef(i = hook.prepatch)语句就不会再去执行了。直接返回false。
代码再往下执行:var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children;
所以 var elm = vnode.elm = oldVnode.elm; 即:var elm = vnode.elm = "div#app"; oldCh = oldVnode.children = []; ch = vnode.children 值变为以下:
ch = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
从上面可知:
vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: 'div#app', key: undefined } oldVnode = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined };
接着执行以下代码:if (oldVnode === vnode) return; 判断若是上一次的虚拟节点和新的虚拟节点相同的话,那就不进行页面渲染操做,直接返回。
继续执行以下代码:
function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } if (!sameVnode(oldVnode, vnode)) { var parentElm = api.parentNode(oldVnode.elm); elm = createElm(vnode, insertedVnodeQueue); api.insertBefore(parentElm, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; }
如上代码判断,若是上一次虚拟节点和新的虚拟节点不相同的话,就执行if语句内部代码,如上sameVnode函数代码咱们也知道,判断虚拟节点是否相同是经过 虚拟节点中的key和sel属性来进行判断的。
sel是选择器的含义,key是每一个标签中自定义的key。
因为oldValue 和 vnode 上面咱们已经知道该值,所以sameVnode(oldVnode, vnode)函数就返回true,最后 !sameVnode(oldVnode, vnode) 就返回false了。说明目前的虚拟节点是相同的。
再接着执行以下代码:
function isDef(s) { return s !== undefined; } if (isDef(vnode.data)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); }
由上面可知咱们的 vnode.data = {style: {color: '#000'}}; 所以 执行 isDef(vnode.data) 值是不等于undefined的,所以返回true。执行if语句内部代码:
由上面分析咱们可知 cbs 的值返回以下数据:
cbs = {
create: [updateClass, updateProps, updateStyle, updateEventListeners],
update: [updateClass, updateProps, updateStyle, updateEventListeners],
remove: [applyRemoveStyle],
destroy: [applyDestroyStyle, updateEventListeners],
pre: [],
post: []
};
cbs.update = [updateClass, updateProps, updateStyle, updateEventListeners]; 会进入for循环。
所以在for循环内部。
由上可知 oldValue 和 vnode的值分别为以下:
oldValue = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined }; vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: 'div#app', key: undefined }
i = 0 时;
执行 cbs.update[0](oldVnode, vnode); 代码; 就会调用 updateClass(oldVnode, vnode) 函数。
updateClass 类在 snabbdom/modules/class.js 内部代码以下:
/* 该函数的做用有2点,以下: 1. 从elm中删除vnode(新虚拟节点)不存在的类名。 2. 将vnode中新增的class添加到elm上去。 */ function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; // 若是旧节点和新节点都没有class的话,直接返回 if (!oldClass && !klass) return; oldClass = oldClass || {}; klass = klass || {}; /* 若是新虚拟节点中找不到该类名,咱们须要从elm中删除该类名 */ for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } /* 若是新虚拟节点的类名在旧虚拟节点中的类名找不到的话,就新增该类名。 不然的话,旧节点能找到该类名的话,就删除该类名,也能够理解为: 对html元素不进行从新渲染操做。 */ for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } } } module.exports = {create: updateClass, update: updateClass};
如上代码咱们能够看到 oldClass = undefined; klass = undefined; 所以无论 i 循环多少次,或等于几,oldClass 和 klass 值都会等于undeinfed的,所以不会执行updateClass内部代码的。
i = 1 时;
执行 cbs.update[1](oldValue, vnode); 代码,所以会调用 updateProps(oldVnode, vnode); 函数。
updateProps 类在 snabbdom/modules/props.js 内部代码以下:
/* 以下函数的做用是: 1. 从elm上删除vnode中不存在的属性。 2. 更新elm上的属性。 */ function updateProps(oldVnode, vnode) { var key, cur, old, elm = vnode.elm, oldProps = oldVnode.data.props, props = vnode.data.props; // 若是新旧虚拟节点都不存在属性的话,就直接返回 if (!oldProps && !props) return; oldProps = oldProps || {}; props = props || {}; /* 若是新虚拟节点中没有该属性的话,则直接从元素中删除该属性。 */ for (key in oldProps) { if (!props[key]) { delete elm[key]; } } // 更新属性 for (key in props) { cur = props[key]; old = oldProps[key]; /* 若是新旧虚拟节点中属性不一样。且对比的属性不是value,能够排除 input, textarea这些标签的value值。及elm上对应的属性和新虚拟 节点的属性不相同的话,那么就须要更新该属性。 */ if (old !== cur && (key !== 'value' || elm[key] !== cur)) { elm[key] = cur; } } } module.exports = {create: updateProps, update: updateProps};
如上代码,咱们继续把oldVnode 和 vnode值传递进去,oldValue.data = {}; vnode.data = {style: {color: '#000'}}; 所以 oldProps = oldVnode.data.props = undefined; props = vnode.data.props = undefined; 所以 执行代码 if (!oldProps && !props) return; 就执行返回了。
i = 2 时
执行 cbs.update[2](oldValue, vnode); 代码,所以会调用 updateStyle(oldVnode, vnode); 函数。
updateStyle 类在 snabbdom/modules/style.js, 部分代码以下所示:
function updateStyle(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldStyle = oldVnode.data.style, style = vnode.data.style; if (!oldStyle && !style) return; oldStyle = oldStyle || {}; style = style || {}; var oldHasDel = 'delayed' in oldStyle; /* 若是旧虚拟节点有style,新虚拟节点没有style,所以elm.style[name] 就置空。 */ for (name in oldStyle) { if (!style[name]) { elm.style[name] = ''; } } /* 若是 vnode.data.style 中有 'delayed'的话,则遍历 style.delayed, 获取其中一项 cur = style.delayed[name]; 也就是说,若是vnode.style 中的delayed和oldvnode不一样的话,则更新delayed的属性值,而且使用 setNextFrame方法在下一帧将elm的style设置为该值,从而实现动画过分 效果。 */ for (name in style) { cur = style[name]; if (name === 'delayed') { for (name in style.delayed) { cur = style.delayed[name]; if (!oldHasDel || cur !== oldStyle.delayed[name]) { setNextFrame(elm.style, name, cur); } } } /* 若是 vnode.data.style 中任何项不是remove , 而且不一样于oldVnode的 值,则直接设置新值。 */ else if (name !== 'remove' && cur !== oldStyle[name]) { elm.style[name] = cur; } } }
由上咱们知道oldVnode 和 vnode的值,所以 oldStyle = oldVnode.data.style; oldVnode.data 值为 {}; 所以 oldStyle = undefined 了; style = vnode.data.style; vnode.data 值为:
vnode.data = {style: {color: '#000'}}; 所以 style = vnode.data.style = {color: '#000'}; 所以代码中的if判断 if (!oldStyle && !style) 返回的false。 代码继续往下执行:
oldStyle = oldStyle || {}; 即 oldStyle = {}; style = style || {}; 即 style = {color: '#000'}; var oldHasDel = 'delayed' in oldStyle; 若是 'delayed' 在 oldStyle 中的话,返回true. 所以这里返回false, 即 oldHasDel = false; 继续执行以下for循环代码:
/* 若是旧虚拟节点有style,新虚拟节点没有style,所以elm.style[name] 就置空。 */ for (name in oldStyle) { if (!style[name]) { elm.style[name] = ''; } }
因为oldStyle 为 {}; 所以不会进入for循环内部,代码直接跳过。再接着继续执行下面的代码:
/* 若是 vnode.data.style 中有 'delayed'的话,则遍历 style.delayed, 获取其中一项 cur = style.delayed[name]; 也就是说,若是vnode.style 中的delayed和oldvnode不一样的话,则更新delayed的属性值,而且使用setNextFrame方法在下一帧将elm的style设置为该值,从而实现动画过分效果。 */ for (name in style) { cur = style[name]; if (name === 'delayed') { for (name in style.delayed) { cur = style.delayed[name]; if (!oldHasDel || cur !== oldStyle.delayed[name]) { setNextFrame(elm.style, name, cur); } } } /* 若是 vnode.data.style 中任何项不是remove , 而且不一样于oldVnode的 值,则直接设置新值。 */ else if (name !== 'remove' && cur !== oldStyle[name]) { elm.style[name] = cur; } }
由上面分析可知,咱们的style值为 {color: '#000'}; 所以遍历style,该name值不会等于 'delayed'; 可是此时 cur = '#000' 了。所以进入 else if 语句代码,而且oldStyle = {}; 所以 cur 确定不等于 oldStyle[name]; 所以 elm.style[name] = cur; 代码就会执行了。也就是说 'div#app' 元素的有样式 style = "{color: '#000'}" 了。
i = 3 时,
执行 cbs.update[3](oldValue, vnode); 代码,所以会调用 updateEventListeners(oldVnode, vnode); 函数。
updateEventListeners 类在 snabbdom/modules/eventlisteners.js, 代码以下所示:
function invokeHandler(handler, vnode, event) { // ....... } function handleEvent(event, vnode) { var name = event.type, on = vnode.data.on; // call event handler(s) if exists if (on && on[name]) { invokeHandler(on[name], vnode, event); } } function createListener() { return function handler(event) { handleEvent(event, handler.vnode); } } // 上面代码是对建立一个事件监听器逻辑 // 更新事件监听 function updateEventListeners(oldVnode, vnode) { var oldOn = oldVnode.data.on, oldListener = oldVnode.listener, oldElm = oldVnode.elm, on = vnode && vnode.data.on, elm = vnode && vnode.elm, name; // optimization for reused immutable handlers // 若是新旧事件监听器同样的话,则直接返回 if (oldOn === on) { return; } // remove existing listeners which no longer used // 若是新节点上没有事件监听器,则将旧节点上的事件监听都删除 if (oldOn && oldListener) { // if element changed or deleted we remove all existing listeners unconditionally if (!on) { for (name in oldOn) { // remove listener if element was changed or existing listeners removed oldElm.removeEventListener(name, oldListener, false); } } else { /* 不然的话,旧节点的事件监听器在新节点上事件监听找不到的话, 则删除旧节点中的事件监听器 */ for (name in oldOn) { // remove listener if existing listener removed if (!on[name]) { oldElm.removeEventListener(name, oldListener, false); } } } } // add new listeners which has not already attached if (on) { // reuse existing listener or create new /* 若是oldVnode 上已经有listener的话,则vnode直接使用,不然的话, 新建事件处理器。 */ var listener = vnode.listener = oldVnode.listener || createListener(); // update vnode for listener // 在事件处理器上更新 vnode listener.vnode = vnode; // if element changed or added we add all needed listeners unconditionally // 若是oldVnode上没有事件处理器的话 if (!oldOn) { /* 且newVnode 是有事件监听器,所以遍历,直接将vnode上的事件处理器 添加到elm上。 */ for (name in on) { // add listener if element was changed or new listeners added elm.addEventListener(name, listener, false); } } else { /* 不然的话,若是oldVnode有事件处理器的话,遍历新 newVnode 节点上 的事件,若是新虚拟节点的事件在 oldVnode 上找不到的话,就把该 事件添加到elm上去。也就是说 oldVnode 上没有的事件,就添加上去。 */ for (name in on) { // add listener if new listener added if (!oldOn[name]) { elm.addEventListener(name, listener, false); } } } } } module.exports = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners };
如上代码调用 updateEventListeners(oldVnode, vnode) 函数,为了方便查看代码,咱们把oldVnode 和 vnode 值再打印下以下所示:
oldValue = { sel: 'div#app', data: {}, children: [], text: undefined, elm: "div#app", key: undefined }; vnode = { sel: 'div#app', data: {style: {color: '#000'}}, children: [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ], text: undefined, elm: 'div#app', key: undefined }
所以执行内部代码:var oldOn = oldVnode.data.on = undefined; oldListener = oldVnode.listener = undefined; oldElm = oldVnode.elm = 'div#app'; on = vnode && vnode.data.on = undefined;
elm = vnode && vnode.elm = 'div#app'; 而后只需以下if判断代码:
if (oldOn === on) { return; }
如上咱们能够看到 oldOn = undefined; on = undefined; 所以代码直接返回了。下面的代码就不会执行了,说明新旧虚拟节点都没有监听器,就不须要更新事件监听器了。
咱们如今把目光视线再回到 snabbdom/snabbdom.js 中的 patchVnode 函数中来,接着执行后面的代码以下:
function isDef(s) { return s !== undefined; } i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
所以 i = vnode.data.hook = undefined 了; 所以 下面的if语句直接返回false了,就不会执行 i(oldVnode, vnode); 这个函数了。 如今代码继续往下执行以下代码:
function isUndef(s) { return s === undefined; } function isDef(s) { return s !== undefined; } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); }
由上可知,vnode.text = undefined; 所以代码 isUndef(vnode.text) 返回true; 执行if语句内部代码,由上分析可知:oldCh = oldVnode.children = []; ch值变为以下:
ch = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ];
所以 oldCh !== ch 为true, 所以会调用 updateChildren(elm, oldCh, ch, insertedVnodeQueue); 方法,该方法的代码以下所示:
/* @param {parentElm} 'div#app' @param {oldCh} [] @param {newCh} newCh = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ]; @param {insertedVnodeQueue} [] */ function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, elmToMove, before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { // New element api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
如上代码,咱们先看一些初始化的代码以下:
var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, elmToMove, before;
如上代码能够推断出 var oldEndIdx = oldCh.length - 1 = -1; var oldStartVnode = oldCh[0] = undefined; var oldEndVnode = oldCh[oldEndIdx] = undefined;
var newEndIdx = newCh.length - 1 = 3 - 1 = 2; var newStartVnode = newCh[0]; 所以 newStartVnode 的值变为以下:
var newStartVnode = { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined };
var newEndVnode = newCh[newEndIdx] = newCh[2]; 所以 newEndVnode 的值变为以下:
newEndVnode = { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined };
所以 咱们再整理下如上初始化的值了:
var oldStartIdx = 0; var newStartIdx = 0; var oldEndIdx = -1; var oldStartVnode = undefined; var oldEndVnode = undefined; var newEndIdx = 2; var newStartVnode = { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }; var newEndVnode = { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined }; var oldKeyToIdx, idxInOld, elmToMove, before;
接下来执行 while 循环语句:
/* 由上分析可知: oldStartIdx = 0; oldEndIdx = -1; newStartIdx = 0; newEndIdx = 2; */ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { }
所以while循环语句返回的是false,不会进入内部代码进行判断。继续执行以下代码:
function isUndef(s) { return s === undefined; } if (oldStartIdx > oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); }
由上分析可知:oldStartIdx = 0; oldEndIdx = -1; 所以会进入if语句代码:执行 before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm;
首先代码 newCh[newEndIdx+1] = newCh[2+1] = newCh[3] = undefined; 所以 isUndef(newCh[newEndIdx+1]) 代码为true; 所以此时 before = null; 接着代码往下执行:
/* @param {parentElm} 'div#app' @param {before} null @param {newCh} newCh = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: undefined, text: ' and xxxx', elm: undefined, key: undefined }, { sel: 'a', data: {props: {href: '/foo'}}, children: undefined, text: "我是空智", elm: undefined, key: undefined } ]; @param {newStartIdx} 0 @param {newEndIdx} 2 @param {insertedVnodeQueue} [] */ addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); 函数。 function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before); } }
参数传递进来后,所以形参各个值分别对应以下:
parentElm = 'div#app'; before = null; vnodes = [ { sel: 'span', data: {style: {fontWeight: 'bold'}}, children: undefined, text: "my name is kongzhi", elm: undefined, key: undefined }, { sel: undefined, data: undefined, children: 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 库对新旧虚拟节点进行对比,而后找出不一样的节点来,而后对不一样的节点进行渲染的整个分析过程。