统共写了四篇文章(都是自身的一些鄙见,仅供参考,请多多指教,我这边也会延续修正加更新)
这篇我将以自身的思绪去解读一下源码(这里的源码我为了兼容IE8有作修正);
对假造dom的明白
经由历程js对象模拟出一个我们须要衬着到页面上的dom树的构造,完成了一个修正js对象即可修正页面dom的快速门路,防止了我们‘手动’再去一次次操纵dom-api的烦琐,而且其供应了算法可以使得用起码的dom操纵举行修正。
从例子动身,寻觅切入点
var snabbdom = SnabbdomModule;
var patch = snabbdom.init([ //导入响应的模块
DatasetModule,
ClassModule,
AttributesModule,
PropsModule,
StyleModule,
EventlistenerModule
]);
var h = HModule.h;
var app = document.getElementById('app');
var newVnode = h('div#divId.red', {}, [h('p', {},'已转变')])
var vnode = h('div#divId.red', {}, [h('p',{},'2S后转变')])
vnode = patch(app, vnode);
setTimeout(function() {
vnode=patch(vnode, newVnode);
}, 2000)
从上面的例子不难看出,我们须要从三个重点函数 init patch h 切入,这三个函数离别的作用是:初始化模块,对照衬着,构建vnode;
而文章开首我说了完成假造dom的第一步就是 经由历程js对象模拟出一个我们须要衬着到页面上的dom树的构造
,所以’首当其冲’就是须要先相识h函数,如何将js对象封装成vnode,vnode是我们定义的假造节点,然后就是应用patch函数举行衬着
构建vnode
h.js
var HModule = {};
(function(HModule) {
var VNode = VNodeModule.VNode;
var is = isModule;
/**
*
* @param sel 选择器
* @param b 数据
* @param childNode 子节点
* @returns {{sel, data, children, text, elm, key}}
*/
//挪用vnode函数将数据封装成假造dom的数据构造并返回,在挪用之前会对数据举行一个处置惩罚:是不是含有数据,是不是含有子节点,子节点范例的推断等
HModule.h = function(sel, b, childNode) {
var data = {},
children, text, i;
if (childNode !== undefined) { //假如childNode存在,则其为子节点
//则h的第二项b就是data
data = b;
if (is.array(childNode)) { //假如子节点是数组,则存在子element节点
children = childNode;
} else if (is.primitive(childNode)) { //不然子节点为text节点
text = childNode;
}
} else if (b !== undefined) { //假如只要b存在,childNode不存在,则b有多是子节点也有多是数据
//数组代表子element节点
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]);
}
}
//返回VNode
return VNode(sel, data, children, text, undefined);
}
})(HModule)
h函数的重要事情就是把传入的参数封装为vnode
接下来看一下,vnode的构造
vnode.js
var VNodeModule = {};
(function(VNodeModule) {
VNodeModule.VNode = 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
};
}
})(VNodeModule)
sel 对应的是选择器,如'div','div#a','div#a.b.c'的情势
data 对应的是vnode绑定的数据,可以有以下范例:attribute、props、eventlistner、
class、dataset、hook
children 子元素数组
text 文本,代表该节点中的文本内容
elm 内里存储着对应的实在dom element的援用
key vnode标识符,重如果用在须要轮回衬着的dom元素在举行diff运算时的优化算法,比方ul>li,tobody>tr>td等
text和children是不会同时存在的,存在text代表子节点仅为文本节点
如:h(‘p’,123)
—> <p>123</p>;存在children代表其子节点存在其他元素节点(也可以包括文本节点),须要将这些节点放入数组中 如:h(‘p’,[h(‘h1′,123),’222’]) —> <p><h1>123</h1>222</p>
打印一下例子中挪用h函数后的构造:
vnode:
newVnode:
关于elm这个值背面再说
初始化模块和对照衬着
应用vnode天生我们的假造dom树后,就须要最先举行衬着了;只所以说是对照衬着,是因为它衬着的机制不是直接把我们的设置好的vnode悉数衬着,而是会举行一次新旧vnode的对照,举行差别衬着;
snabbdom.js
init函数
function init(modules, api) {
...
}
它有两个参数,第一个是须要加载的模块数组,第二个是操纵dom的api,平常我们只须要传入第一个参数即可
1.模块的初始化
先拿个模块举例:
var ClassModule = {};
function updateClass(oldVnode, vnode){}
ClassModule.create = updateClass;
ClassModule.update = updateClass;
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; //全局钩子:modules自带的钩子函数
function init(modules, api) {
var i, j, cbs = {};
...
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]]);
}
}
...
}
上面就是模块初始化的中心,事先在模块中定义好钩子函数(即模块关于vnode的操纵),然后在init函数中顺次将这些模块的钩子函数加载进来,放在一个对象中保存,守候挪用;
ps:init函数内里还会定义一些功用函数,等用到的时刻再说,然后下一个须要剖析的就是init被挪用后会return一个函数—patch函数(这个函数是自身定义的一个变量名);
2.挪用patch函数举行对照衬着
在没看源码之前,我一向认为snabbdom的对照衬着是会把新旧vnode对照效果发生一个差别对象,然后在应用这个差别对象再举行衬着,背面看了后发明snabbdom这边是在对照的同时就直接应用dom的API在旧的dom上举行修正,而这些操纵(衬着)就是定义在我们前面加载的模块中。
这里须要说一下snabbdom的对照战略是针对同层级的节点
举行对照
实在这里就有一个小知识点,bfs---广度优先遍历
广度优先遍历从某个极点动身,起首接见这个极点,然后找出这个结点的一切未被接见的邻接点,接见完后再接见这些结点中第一个邻接点的一切结点,反复此要领,直到一切结点都被接见完为止。
网上引见的文章许多,我这边就不过量引见了;
举个例子
var tree = {
val: 'div',
ch: [{
val: 'p',
ch: [{
val: 'text1'
}]
}, {
val: 'p',
ch: [{
val: 'span',
ch: [{
val: 'tetx2'
}]
}]
}]
}
function bfs(tree) {
var queue = [];
var res = []
if (!tree) return
queue.push(tree);
while (queue.length) {
var node = queue.shift();
if (node.ch) {
for (var i = 0; i < node.ch.length; i++) {
queue.push(node.ch[i]);
}
}
if (node.val) {
res.push(node.val);
}
}
return res;
}
console.log(bfs(tree)) //["div", "p", "p", "text1", "span", "tetx2"]
思绪:先把根节点放入一个数组queue中,然后将其取出来,推断其是不是有子节点,假如有,将其子节点顺次放入queue数组中;然后顺次再从这个数组中取值,反复上述步骤,直到这个数组queue没有数据;
这里snabbdom会比较每个节点它的sel是不是类似,假如类似对其子节点再举行比较,不然直接删除这个节点,增加新节点,其子节点也不会继续举行比较
patch函数
return function(oldVnode, vnode) {
var i, elm, parent;
//纪录被插进去的vnode行列,用于批量触发insert
var insertedVnodeQueue = [];
//挪用全局pre钩子
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
//假如oldvnode是实在的dom节点,则转化为一个空vnode,平常这是初始化衬着的时刻会用到
if (isUndef(oldVnode.sel)) {
oldVnode = emptyNodeAt(oldVnode);
}
//假如oldvnode与vnode类似,举行更新;类似是比较其key值与sel值
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
//不然,将新的vnode插进去,并将oldvnode从其父节点上直接删除
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);
}
}
//插进去完后,挪用被插进去的vnode的insert钩子
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
//然后挪用全局下的post钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
//返回vnode用作下次patch的oldvnode
return vnode;
};
流程图:
- 当oldvnode的sel为空的时刻,这里涌现的场景基础上就是我们第一次挪用patch去初始化衬着页面
- 比较类似的体式格局为vnode的sel,key两个属性是不是相称,不定义key值也没紧要,因为不定义则为undefined,而undefined===undefined,只须要sel相称即可类似
- 因为比较战略是同层级比较,所以当父节点不相类似时,子节点也不会再去比较
- 末了会将vnode返回,也就是我们现在须要衬着到页面上的vnode,它将会作为下一次衬着时的oldvnode
这基础上就是一个对照的大致历程,值得研讨的东西还在背面,触及到了其中心的diff算法,下篇文章再提。
再引见一下上面用到的一些功用函数:
isUndef
为is.js中的函数,用来推断数据是不是为undefined
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);
}
用来将一个实在的无子节点的DOM节点转化成vnode情势,
如:<div id=’a’ class=’b c’></div>
将转换为{sel:’div#a.b.c’,data:{},children:[],text:undefined,elm:<div id=’a’ class=’b c’>}
sameVnode
function sameVnode(vnode1, vnode2) {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
用来比较两个vnode是不是类似。
假如新旧vnode的key和sel都雷同,申明两个vnode类似,我们就可以保存旧的vnode节点,再详细去比较其差别性,在旧的vnode上举行’打补丁’,不然直接替换节点。这里须要说的是假如不定义key值,则这个值就为undefined,undefined===undefined //true,所以日常平凡在用vue的时刻,在没有用v-for衬着的组件的条件下,是不须要定义key值的,不会影响其比较。
createElm
建立vnode对应的实在dom,并将其赋值给vnode.elm,后续关于dom的修正都是在这个值上举行
//将vnode建立为实在dom
function createElm(vnode, insertedVnodeQueue) {
var i, data = vnode.data;
if (isDef(data)) {
//当节点上存在hook而且hook中有beforeCreate钩子时,先挪用beforeCreate回调,对刚建立的vnode举行处置惩罚
if (isDef(i = data.hook) && isDef(i = i.beforeCreate)) {
i(vnode);
//猎取beforeCreate钩子修正后的数据
data = vnode.data;
}
}
var elm, children = vnode.children,
sel = vnode.sel;
if (isDef(sel)) {
//剖析sel参数,比方div#divId.divClass ==>id="divId" class="divClass"
var hashIdx = sel.indexOf('#');
//先id后class
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;
//建立一个DOM节点援用,并对其属性实例化
elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag) : api.createElement(tag);
//猎取id名 #a --> a
if (hash < dot) elm.id = sel.slice(hash + 1, dot);
//猎取类名,并格式化 .a.b --> a b
if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' ');
//假如存在子元素Vnode节点,则递归将子元素节点插进去到当前Vnode节点中,并将已插进去的子元素节点在insertedVnodeQueue中作纪录
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
api.appendChild(elm, createElm(children[i], insertedVnodeQueue));
}
} else if (is.primitive(vnode.text)) { //假如存在子文本节点,则直接将其插进去到当前Vnode节点
api.appendChild(elm, api.createTextNode(vnode.text));
}
//当建立终了后,触发全局create钩子回调
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
i = vnode.data.hook; // Reuse variable
if (isDef(i)) { //触发自身的create钩子回调
if (i.create) i.create(emptyNode, vnode);
//假如有insert钩子,则推动insertedVnodeQueue中作纪录,从而完成批量插进去触发insert回调
if (i.insert) insertedVnodeQueue.push(vnode);
}
}
//假如没声明选择器,则申明这个是一个text节点
else {
elm = vnode.elm = api.createTextNode(vnode.text);
}
return vnode.elm;
}
patchVnode
假如两个vnode类似,则会对详细的vnode举行‘打补丁’的操纵
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
var i, hook;
//在patch之前,先挪用vnode.data的beforePatch钩子
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.beforePatch)) {
i(oldVnode, vnode);
}
var elm = vnode.elm = oldVnode.elm,
oldCh = oldVnode.children,
ch = vnode.children;
//假如oldnode和vnode的援用雷同,申明没发生任何变化直接返回,防止机能糟蹋
if (oldVnode === vnode) return;
//假如oldvnode和vnode差别,申明vnode有更新
//假如vnode和oldvnode不类似则直接用vnode援用的DOM节点去替换oldvnode援用的旧节点
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;
}
//假如vnode和oldvnode类似,那末我们要对oldvnode自身举行更新
if (isDef(vnode.data)) {
//起首挪用全局的update钩子,对vnode.elm自身属性举行更新
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
//然后挪用vnode.data内里的update钩子,再次对vnode.elm更新
i = vnode.data.hook;
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
}
/*
分状况议论节点的更新: new代表新Vnode old代表旧Vnode
ps:假如自身存在文本节点,则不存在子节点 即:有text则不会存在ch,反之亦然
1 new不为文本节点
1.1 new不为文本节点,new还存在子节点
1.1.1 new不为文本节点,new还存在子节点,old有子节点
1.1.2 new不为文本节点,new还存在子节点,old没有子节点
1.1.2.1 new不为文本节点,new还存在子节点,old没有子节点,old为文本节点
1.2 new不为文本节点,new不存在子节点
1.2.1 new不为文本节点,new不存在子节点,old存在子节点
1.2.2 new不为文本节点,new不存在子节点,old为文本节点
2.new为文本节点
2.1 new为文本节点,而且old与new的文本节点不相称
ps:这里只须要议论这一种状况,因为假如old存在子节点,那末文本节点text为undefined,则与new的text不相称
直接node.textContent即可清晰old存在的子节点。若old存在子节点,且相称则无需修正
*/
//1
if (isUndef(vnode.text)) {
//1.1.1
if (isDef(oldCh) && isDef(ch)) {
//当Vnode和oldvnode的子节点差别时,挪用updatechilren函数,diff子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
}
//1.1.2
else if (isDef(ch)) {
//oldvnode是text节点,则将elm的text消灭
//1.1.2.1
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
//并增加vnode的children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
}
//假如oldvnode有children,而vnode没children,则移除elm的children
//1.2.1
else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
//1.2.2
//假如vnode和oldvnode都没chidlren,且vnode没text,则删除oldvnode的text
else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
}
//假如oldvnode的text和vnode的text差别,则更新为vnode的text,
//2.1
else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm, vnode.text);
}
//patch完,触发postpatch钩子
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode);
}
}
removeVnodes
/*
这个函数重要功用是批量删除DOM节点,须要合营invokeDestoryHook和createRmCb
重要步骤以下:
挪用invokeDestoryHook以触发destory回调
挪用createRmCb来最先对remove回调举行计数
删除DOM节点
*
*
* @param parentElm 父节点
* @param vnodes 删除节点数组
* @param startIdx 删除肇端坐标
* @param endIdx 删除完毕坐标
*/
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var i, listeners, rm, ch = vnodes[startIdx]; //ch代表子节点
if (isDef(ch)) {
if (isDef(ch.sel)) {
//挪用destroy钩子
invokeDestroyHook(ch);
//对全局remove钩子举行计数
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm, listeners);
//挪用全局remove回调函数,并每次削减一个remove钩子计数
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
//挪用内部vnode.data.hook中的remove钩子(只要一个)
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
i(ch, rm);
} else {
//假如没有内部remove钩子,须要挪用rm,确保可以remove节点
rm();
}
} else { // Text node
api.removeChild(parentElm, ch.elm);
}
}
}
}
invokeDestroyHook
/*
这个函数用于手动触发destory钩子回调,重要步骤以下:
先挪用vnode上的destory
再挪用全局下的destory
递归挪用子vnode的destory
*/
function invokeDestroyHook(vnode) {
var i, j, data = vnode.data;
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); //挪用自身的destroy钩子
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); //挪用全局destroy钩子
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j]);
}
}
}
}
addVnodes
//将vnode转换后的dom节点插进去到dom树的指定位置中去
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
}
}
createRmCb
/*
remove一个vnode时,会触发remove钩子作拦截器,只要在一切remove钩子
回调函数都触发完才会将节点从父节点删除,而这个函数供应的就是对remove钩子回调操纵的计数功用
*/
function createRmCb(childElm, listeners) {
return function() {
if (--listeners === 0) {
var parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
另有一个最中心的函数updateChildren
,这个留到下篇文章再说;
我们这边简朴的总结一下:
对照衬着的流程大致分为
1.经由历程sameVnode来推断两个vnode是不是值得举行比较
2.假如不值得,直接删除旧的vnode,衬着新的vnode
3.假如值得,挪用模块钩子函数,对其节点的属性举行替换,比方style,event等;再推断节点子节点是不是为文本节点,假如为文本节点则举行更替,假如还存在其他子节点则挪用updateChildren,对子节点举行更新,更新流程将会回到第一步,反复;