媒介
近来在进修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自身!