一: 什么是 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 地址为:, 我这边下载的是 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 = 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 库对新旧虚拟节点进行对比, 然后找出不同的节点来, 然后对不同的节点进行渲染的整个分析过程.
来源: https://www.cnblogs.com/tugenhua0707/p/11762585.html