前言
最近在学习vue2.0的源码,刚开始看其vdom源码,着实找不到方向,因为其在vdom的实现上还加
入了很多vue2.0本身的钩子,加大了阅读难度。于是看到第一行尤大说vue2.0的vdom是在snabbdom
的基础上改过来的,而snabbdom只有不到300sloc,那不妨先从snabbdom入手,熟悉其中的原理,
再配合vue2.0的vdom看,效果可能更好。
什么是virtual-dom
virtual-dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无
状态的组件,当组件状态发生更新时,然后触发virtual-dom数据的变化,然后通过virtual-dom
和真实DOM的比对,再对真实dom更新。
为什么是virtual-dom
我们知道,当我们希望实现一个具有复杂状态的界面时,如果我们在每个可能发生变化的组件上都绑定
事件,绑定字段数据,那么很快由于状态太多,我们需要维护的事件和字段将会越来越多,代码也会
越来越复杂,于是,我们想我们可不可以将视图和状态分开来,只要视图发生变化,对应状态也发生
变化,然后状态变化,我们再重绘整个视图就好了。这样的想法虽好,但是代价太高了,于是我们又
想,能不能只更新状态发生变化的视图?于是virtual-dom应运而生,状态变化先反馈到vdom上,
vdom在找到最小更新视图,最后批量更新到真实DOM上,从而达到性能的提升。
除此之外,从移植性上看,virtual-dom还对真实dom做了一次抽象,这意味着virtual-dom对应
的可以不是浏览器的dom,而是不同设备的组件,极大的方便了多平台的使用。
snabbdom目录结构
好了,说了这么多,我们先来看看snabbdom吧,我看的是这个版本的snabbdom
(心塞,typescript学的不深,看最新版的有点吃力,所以选了ts版本前的一个版本)。好了我们先
看看snabbdom的主要目录结构。
名称 | 类型 | 解释 |
---|---|---|
dist | 文件夹 | 里面包含了snabddom打包后的文件 |
examples | 文件夹 | 里面包含了使用snabbdom的例子 |
helpers | 文件夹 | 包含svg操作需要的工具 |
modules | 文件夹 | 包含了对attribute,props,class,dataset,eventlistner,style,hero的操作 |
perf | 文件夹 | 性能测试 |
test | 文件夹 | 测试 |
h | 文件 | 把状态转化为vnode |
htmldomapi | 文件 | 原生dom操作的抽象 |
is | 文件 | 判断类型 |
snabbdom.bundle | 文件 | snabbdom本身依赖打包 |
snabbdom | 文件 | snabbdom 核心,包含diff,patch等操作 |
thunk | 文件 | snabbdom下的thunk功能实现 |
vnode | 文件 | 构造vnode |
snabbdom源码之旅
第一站 vnode
首先,我们从最简单的vnode开始入手,vnode实现的功能非常简单,就是讲输入的数据转化为vnode
对象的形式
//VNode函数,用于将输入转化成VNode
/**
*
* @param sel 选择器
* @param data 绑定的数据
* @param children 子节点数组
* @param text 当前text节点内容
* @param elm 对真实dom element的引用
* @returns {{sel: *, data: *, children: *, text: *, elm: *, key: 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
};
};
vnode主要有5大属性:
sel 对应的是选择器,如'div','div#a','div#a.b.c'的形式
data 对应的是vnode绑定的数据,可以有以下类型:attribute、props、eventlistner、
class、dataset、hookchildren 子元素数组
text 文本,代表该节点中的文本内容
elm 里面存储着对对应的真实dom element的引用
key 用于不同vnode之间的比对
第二站 h
说完vnode,就到h了,h也是一个包装函数,主要是在vnode上再做一层包装,实现功能如下
如果是svg,则为其添加命名空间
-
将children中的text包装成vnode形式
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渲染为VDOM /** * * @param sel 选择器 * @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 ) { //那么h的第二项就是data data = b; //如果c是数组,那么存在子element节点 if ( is.array ( c ) ) { children = c; } //否则为子text节点 else if ( is.primitive ( c ) ) { text = c; } } //如果c不存在,只存在b,那么说明需要渲染的vdom不存在data部分,只存在子节点部分 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) { //如果子节点数组中,存在节点是原始类型,说明该节点是text节点,因此我们将它渲染为一个只包含text的VNode if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] ); } } //如果是svg,需要为节点添加命名空间 if ( sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' ) { addNS ( data, children, sel ); } return VNode ( sel, data, children, text, undefined ); };
第三站 htmldomapi
htmldomapi中提供了对原生dom操作的一层抽象,这里就不再阐述了
第四站 modules
modules中主要包含attributes,class,props,dataset,eventlistener,hero,style
这些模块,其中attributes,class,props,dataset,eventlistener,style这些模块是我们
日常所需要的,也是snabbdom.bundle默认注入的也是这几个,这里就详细介绍这几个模块
attributes
主要功能如下:
从elm的属性中删除vnode中不存在的属性(包括那些boolean类属性,如果新vnode设置为false,同样删除)
如果oldvnode与vnode用同名属性,则在elm上更新对应属性值
如果vnode有新属性,则添加到elm中
-
如果存在命名空间,则用
setAttributeNS
设置var NamespaceURIs = { "xlink": "http://www.w3.org/1999/xlink" }; var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare", "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable", "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple", "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly", "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate", "truespeed", "typemustmatch", "visible"]; var booleanAttrsDict = Object.create(null); //创建属性字典,默认为true for(var i=0, len = booleanAttrs.length; i < len; i++) { booleanAttrsDict[booleanAttrs[i]] = true; } function updateAttrs(oldVnode, vnode) { var key, cur, old, elm = vnode.elm, oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs, namespaceSplit; //如果旧节点和新节点都不包含属性,立刻返回 if (!oldAttrs && !attrs) return; oldAttrs = oldAttrs || {}; attrs = attrs || {}; // update modified attributes, add new attributes //更新改变了的属性,添加新的属性 for (key in attrs) { cur = attrs[key]; old = oldAttrs[key]; //如果旧的属性和新的属性不同 if (old !== cur) { //如果是boolean类属性,当vnode设置为falsy value时,直接删除,而不是更新值 if(!cur && booleanAttrsDict[key]) elm.removeAttribute(key); else { //否则更新属性值或者添加属性 //如果存在命名空间 namespaceSplit = key.split(":"); if(namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0])) elm.setAttributeNS(NamespaceURIs[namespaceSplit[0]], key, cur); else elm.setAttribute(key, cur); } } } //remove removed attributes // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) // the other option is to remove all attributes with value == undefined //删除不在新节点属性中的旧节点的属性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key); } } } module.exports = {create: updateAttrs, update: updateAttrs};
class
主要功能如下:
从elm中删除vnode中不存在的或者值为false的类
-
将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 || {}; //从旧节点中删除新节点不存在的类 for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } //如果新节点中对应旧节点的类设置为false,则删除该类,如果新设置为true,则添加该类 for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } } } module.exports = {create: updateClass, update: updateClass};
dataset
主要功能如下:
从elm中删除vnode不存在的属性集中的属性
-
更新属性集中的属性值
function updateDataset(oldVnode, vnode) { var elm = vnode.elm, oldDataset = oldVnode.data.dataset, dataset = vnode.data.dataset, key //如果新旧节点都没数据集,则直接返回 if (!oldDataset && !dataset) return; oldDataset = oldDataset || {}; dataset = dataset || {}; //删除旧节点中在新节点不存在的数据集 for (key in oldDataset) { if (!dataset[key]) { delete elm.dataset[key]; } } //更新数据集 for (key in dataset) { if (oldDataset[key] !== dataset[key]) { elm.dataset[key] = dataset[key]; } } } module.exports = {create: updateDataset, update: updateDataset}
eventlistener
snabbdom中对事件处理做了一层包装,真实DOM的事件触发的是对vnode的操作,主要途径是:
createListner => 返回handler作事件监听生成器 =>handler上绑定vnode =>将handler作真实DOM的事件处理器
真实DOM事件触发后 => handler获得真实DOM的事件对象 => 将真实DOM事件对象传入handleEvent => handleEvent找到
对应的vnode事件处理器,然后调用这个处理器从而修改vnode
//snabbdom中对事件处理做了一层包装,真实DOM的事件触发的是对vnode的操作
//主要途径是
// createListner => 返回handler作事件监听生成器 =>handler上绑定vnode =>将handler作真实DOM的事件处理器
//真实DOM事件触发后 => handler获得真实DOM的事件对象 => 将真实DOM事件对象传入handleEvent => handleEvent找到
//对应的vnode事件处理器,然后调用这个处理器从而修改vnode
//对vnode进行事件处理
function invokeHandler ( handler, vnode, event ) {
if ( typeof handler === "function" ) {
// call function handler
//将事件处理器在vnode上调用
handler.call ( vnode, event, vnode );
}
//存在事件绑定数据或者存在多事件处理器
else if ( typeof handler === "object" ) {
//说明只有一个事件处理器
if ( typeof handler[ 0 ] === "function" ) {
//如果绑定数据只有一个,则直接将数据用call的方式调用,提高性能
//形如on:{click:[handler,1]}
if ( handler.length === 2 ) {
handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode );
}
//如果存在多个绑定数据,则要转化为数组,用apply的方式调用,而apply性能比call差
//形如:on:{click:[handler,1,2,3]}
else {
var args = handler.slice ( 1 );
args.push ( event );
args.push ( vnode );
handler[ 0 ].apply ( vnode, args );
}
} else {
//如果存在多个相同事件的不同处理器,则递归调用
//如on:{click:[[handeler1,1],[handler,2]]}
for (var i = 0; i < handler.length; i++) {
invokeHandler ( handler[ i ] );
}
}
}
}
/**
*
* @param event 真实dom的事件对象
* @param vnode
*/
function handleEvent ( event, vnode ) {
var name = event.type,
on = vnode.data.on;
// 如果找到对应的vnode事件处理器,则调用
if ( on && on[ name ] ) {
invokeHandler ( on[ name ], vnode, event );
}
}
//事件监听器生成器,用于处理真实DOM事件
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 ) {
for (name in on) {
// add listener if element was changed or new listeners added
//直接将vnode上的事件处理器添加到elm上
elm.addEventListener ( name, listener, false );
}
} else {
for (name in on) {
// add listener if new listener added
//否则添加oldvnode上没有的事件处理器
if ( !oldOn[ name ] ) {
elm.addEventListener ( name, listener, false );
}
}
}
}
}
module.exports = {
create: updateEventListeners,
update: updateEventListeners,
destroy: updateEventListeners
};
props
主要功能:
从elm上删除vnode中不存在的属性
-
更新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或者elm上对应属性和新属性也不同,那么就需要更新 if (old !== cur && (key !== 'value' || elm[key] !== cur)) { elm[key] = cur; } } } module.exports = {create: updateProps, update: updateProps};
style
主要功能如下:
将elm上存在于oldvnode中但不存在于vnode中不存在的style置空
如果vnode.style中的delayed与oldvnode的不同,则更新delayed的属性值,并在下一帧将elm的style设置为该值,从而实现动画过渡效果
非delayed和remove的style直接更新
vnode被destroy时,直接将对应style更新为vnode.data.style.destory的值
-
vnode被reomve时,如果style.remove不存在,直接调用全局remove钩子进入下一个remove过程
如果style.remove存在,那么我们就需要设置remove动画过渡效果,等到过渡效果结束之后,才调用
下一个remove过程//如果存在requestAnimationFrame,则直接使用,以优化性能,否则用setTimeout var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; var nextFrame = function(fn) { raf(function() { raf(fn); }); }; //通过nextFrame来实现动画效果 function setNextFrame(obj, prop, val) { nextFrame(function() { obj[prop] = val; }); } function updateStyle(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldStyle = oldVnode.data.style, style = vnode.data.style; //如果oldvnode和vnode都没有style,直接返回 if (!oldStyle && !style) return; oldStyle = oldStyle || {}; style = style || {}; var oldHasDel = 'delayed' in oldStyle; //遍历oldvnode的style for (name in oldStyle) { //如果vnode中无该style,则置空 if (!style[name]) { elm.style[name] = ''; } } //如果vnode的style中有delayed且与oldvnode中的不同,则在下一帧设置delayed的参数 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); } } } //如果不是delayed和remove的style,且不同于oldvnode的值,则直接设置新值 else if (name !== 'remove' && cur !== oldStyle[name]) { elm.style[name] = cur; } } } //设置节点被destory时的style function applyDestroyStyle(vnode) { var style, name, elm = vnode.elm, s = vnode.data.style; if (!s || !(style = s.destroy)) return; for (name in style) { elm.style[name] = style[name]; } } //删除效果,当我们删除一个元素时,先回调用删除过度效果,过渡完才会将节点remove function applyRemoveStyle(vnode, rm) { var s = vnode.data.style; //如果没有style或没有style.remove if (!s || !s.remove) { //直接调用rm,即实际上是调用全局的remove钩子 rm(); return; } var name, elm = vnode.elm, idx, i = 0, maxDur = 0, compStyle, style = s.remove, amount = 0, applied = []; //设置并记录remove动作后删除节点前的样式 for (name in style) { applied.push(name); elm.style[name] = style[name]; } compStyle = getComputedStyle(elm); //拿到所有需要过渡的属性 var props = compStyle['transition-property'].split(', '); //对过渡属性计数,这里applied.length >=amount,因为有些属性是不需要过渡的 for (; i < props.length; ++i) { if(applied.indexOf(props[i]) !== -1) amount++; } //当过渡效果的完成后,才remove节点,调用下一个remove过程 elm.addEventListener('transitionend', function(ev) { if (ev.target === elm) --amount; if (amount === 0) rm(); }); } module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};
第五站 is
啃完modules这些大部头,总算有个比较好吃的甜品了,他主要功能就是判断是否为array类型或者原始类型
//is工具库,用于判断是否为array或者原始类型
module.exports = {
array: Array.isArray,
primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; },
};
中途休息
看了这么多源码,估计也累了吧,毕竟一下完全理解可能有点难,不妨先休息一下,消化一下,下一章将会见到最大的boss——snabbdom本身!