媒介
在上一章我们进修了,modules,vnode,h,htmldomapi,is等模块,在这一篇我们将会进修到
snabbdom的中心功用——patchVnode和updateChildren功用。
继承我们的snabbdom源码之旅
终究章 snabbdom!
起首我们先从简朴的部份最先,比方一些东西函数,我将一一来解说他们的用途
sameNode
这个函数主要用于比较oldvnode与vnode同条理节点的比较,假如同条理节点的key和sel都雷同
我们就能够保存这个节点,不然直接替换节点
function sameVnode(vnode1, vnode2) {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
createKeyToOldIdx
这个函数的功用非常简朴,就是将oldvnode数组中位置对oldvnode.key的映照转换为oldvnode.key
对位置的映照
function createKeyToOldIdx(children, beginIdx, endIdx) {
var i, map = {}, key;
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key;
if (isDef(key)) map[key] = i;
}
return map;
}
hook
snabbdom在全局下有6种范例的钩子,触发这些钩子时,会挪用对应的函数对节点的状况举行变动
起首我们来看看有哪些钩子:
Name | Triggered when | Arguments to callback |
---|---|---|
pre | the patch process begins (patch最先时触发) | none |
init | a vnode has been added (vnode被建立时触发) | vnode |
create | a DOM element has been created based on a vnode (vnode转换为实在DOM节点时触发 | emptyVnode, vnode |
insert | an element has been inserted into the DOM (插进去到DOM树时触发) | vnode |
prepatch | an element is about to be patched (元素预备patch前触发) | oldVnode, vnode |
update | an element is being updated (元素更新时触发) | oldVnode, vnode |
postpatch | an element has been patched (元素patch完触发) | oldVnode, vnode |
destroy | an element is directly or indirectly being removed (元素被删除时触发) | vnode |
remove | an element is directly being removed from the DOM (元素从父节点删除时触发,和destory略有差别,remove只影响到被移除节点中最顶层的节点) | vnode, removeCallback |
post | the patch process is done (patch完成后触发) | none |
然后,下面列出钩子对应的状况更新函数:
create => style,class,dataset,eventlistener,props,hero
update => style,class,dataset,eventlistener,props,hero
remove => style
destory => eventlistener,style,hero
pre => hero
post => hero
好了,简朴的都看完了,接下来我们最先打大boss了,第一关就是init函数了
init
init函数有两个参数modules和api,个中modules是init依靠的模块,如attribute、props
、eventlistener这些模块,api则是对封装实在DOM操纵的东西函数库,假如我们没有传入,则默许
运用snabbdom供应的htmldomapi。init还包含了很多vnode和实在DOM之间的操纵和注册全局钩子,
另有patchVnode和updateChildren这两个主要功用,然后返回一个patch函数
注册全局钩子
//注册钩子的回调,在发作状况变动时,触发对应属性变动
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]]);
}
}
emptyNodeAt
这个函数主要的功用是将一个实在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'>}
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);
}
createRmCb
我们晓得当我们须要remove一个vnode时,会触发remove钩子作拦截器,只要在一切remove钩子
回调函数都触发完才会将节点从父节点删除,而这个函数供应的就是对remove钩子回调操纵的计数功用
function createRmCb(childElm, listeners) {
return function() {
if (--listeners === 0) {
var parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
invokeDestoryHook
这个函数用于手动触发destory钩子回调,主要步骤以下:
先挪用vnode上的destory
再挪用全局下的destory
递归挪用子vnode的destory
function invokeDestroyHook(vnode) { var i, j, data = vnode.data; if (isDef(data)) { //先触发该节点上的destory回调 if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); //在触发全局下的destory回调 for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); //递归触发子节点的destory回调 if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]); } } } }
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]; 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); } } } }
createElm
就如太极有阴就有阳一样,既然我们有remove操纵,一定也有createelm的操纵,这个函数主要功用
以下:
初始化vnode,挪用init钩子
建立对应tagname的DOM element节点,并将vnode.sel中的id名和class名挂载上去
假如有子vnode,递归建立DOM element节点,并增加到父vnode对应的element节点上去,
不然假如有text属性,则建立text节点,并增加到父vnode对应的element节点上去
vnode转换成dom节点操纵完成后,挪用create钩子
假如vnode上有insert钩子,那末就将这个vnode放入insertedVnodeQueue中作纪录,到时
再在全局批量挪用insert钩子回调
function createElm(vnode, insertedVnodeQueue) { var i, data = vnode.data; if (isDef(data)) { //当节点上存在hook而且hook中有init钩子时,先挪用init回调,对刚建立的vnode举行处置惩罚 if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); //猎取init钩子修改后的数据 data = vnode.data; } } var elm, children = vnode.children, sel = vnode.sel; if (isDef(sel)) { // Parse selector 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)); } //假如存在子文本节点,则直接将其插进去到当前Vnode节点 } else if (is.primitive(vnode.text)) { 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)) { 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; }
addVnodes
这个函数非常简朴,就是将vnode转换后的dom节点插进去到dom树的指定位置中去
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
}
}
说完上面的节点东西函数以后,我们就最先看怎样举行patch操纵了,起首我们从patch,也就是init
返回的函数最先
patch
起首我们须要明白的一个是,假如根据传统的diff算法,那末为了找到最小变化,须要逐层逐层的去
搜刮比较,如许时候复杂度将会到达 O(n^3)的级别,价值非常高,考虑到节点变化很少是跨条理的,
vdom采用的是一种简化的思绪,只比较同层节点,假如差别,那末纵然该节点的子节点没变化,我们
也不复用,直接将从父节点最先的子树悉数删除,然后再从新建立节点增加到新的位置。假如父节点
没变化,我们就比较一切同层的子节点,对这些子节点举行删除、建立、移位操纵。有了这个头脑,
明白patch也非常简朴了。patch只须要对两个vnode举行推断是不是相似,假如相似,则对他们举行
patchVnode操纵,不然直接用vnode替换oldvnode。
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节点,转化为oldvnode
if (isUndef(oldVnode.sel)) {
oldVnode = emptyNodeAt(oldVnode);
}
//假如oldvnode与vnode相似,举行更新
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;
};
patchVnode
真正对vnode内部patch的照样得靠patchVnode。让我们看看他究竟做了什么?
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
var i, hook;
//在patch之前,先挪用vnode.data的prepatch钩子
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;
//假如oldvnode和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);
}
//假如vnode不是text节点
if (isUndef(vnode.text)) {
//假如vnode和oldVnode都有子节点
if (isDef(oldCh) && isDef(ch)) {
//当Vnode和oldvnode的子节点差别时,挪用updatechilren函数,diff子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
}
//假如vnode有子节点,oldvnode没子节点
else if (isDef(ch)) {
//oldvnode是text节点,则将elm的text消灭
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
//并增加vnode的children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
}
//假如oldvnode有children,而vnode没children,则移除elm的children
else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
//假如vnode和oldvnode都没chidlren,且vnode没text,则删除oldvnode的text
else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
}
//假如oldvnode的text和vnode的text差别,则更新为vnode的text
else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm, vnode.text);
}
//patch完,触发postpatch钩子
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode);
}
}
updateChildren
关于同层的子节点,snabbdom主要有删除、建立的操纵,同时经由过程移位的要领,到达最大复用存在
节点的目标,个中须要保护四个索引,分别是:
oldStartIdx => 旧头索引
oldEndIdx => 旧尾索引
newStartIdx => 新头索引
newEndIdx => 新尾索引
然后最先将旧子节点组和新子节点组举行一一比对,直到遍历完任一子节点组,比对战略有5种:
oldStartVnode和newStartVnode举行比对,假如相似,则举行patch,然后新旧头索引都后移
oldEndVnode和newEndVnode举行比对,假如相似,则举行patch,然后新旧尾索引前移
oldStartVnode和newEndVnode举行比对,假如相似,则举行patch,将旧节点移位到末了
然后旧头索引后移,尾索引前移,为何要如许做呢?我们思索一种状况,如旧节点为【5,1,2,3,4】 ,新节点为【1,2,3,4,5】,假如缺少这类推断,意味着须要先将5->1,1->2,2->3,3->4,4->5五 次删除插进去操纵,纵然是有了key-index来复用,也会涌现也会涌现【5,1,2,3,4】-> 【1,5,2,3,4】->【1,2,5,3,4】->【1,2,3,5,4】->【1,2,3,4,5】共4次操纵,假如 有了这类推断,我们只须要将5插进去到旧尾索引背面即可,从而完成右移
oldEndVnode和newStartVnode举行比对,处置惩罚和上面相似,只不过改成左移
假如以上状况都失利了,我们就只能复用key雷同的节点了。起首我们要经由过程createKeyToOldIdx
建立key-index的映照,假如新节点在旧节点中不存在,我们将它插进去到旧头索引节点前, 然后新头索引向后;假如新节点在就旧节点组中存在,先找到对应的旧节点,然后patch,并将 旧节点组中对应节点设置为undefined,代表已遍历过了,不再遍历,不然能够存在反复 插进去的题目,末了将节点移位到旧头索引节点之前,新头索引向后
遍历完以后,将盈余的新Vnode增加到末了一个新节点的位置后或许删除过剩的旧节点
/**
*
* @param parentElm 父节点
* @param oldCh 旧节点数组
* @param newCh 新节点数组
* @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)) {
//对旧头索引节点和新头索引节点举行diff更新, 从而到达复用节点结果
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];
}
//假如旧头索引节点和新头索引节点相似,能够经由过程挪动来复用
//如旧节点为【5,1,2,3,4】,新节点为【1,2,3,4,5】,假如缺少这类推断,意味着
//那样须要先将5->1,1->2,2->3,3->4,4->5五次删除插进去操纵,纵然是有了key-index来复用,
// 也会涌现【5,1,2,3,4】->【1,5,2,3,4】->【1,2,5,3,4】->【1,2,3,5,4】->【1,2,3,4,5】
// 共4次操纵,假如有了这类推断,我们只须要将5插进去到末了一次操纵即可
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];
}
//假如上面的推断都不经由过程,我们就须要key-index表来到达最大水平复用了
else {
//假如不存在旧节点的key-index表,则建立
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);
//然后将旧节点组中对应节点设置为undefined,代表已遍历过了,不在遍历,不然能够存在反复插进去的题目
oldCh[idxInOld] = undefined;
//插进去到旧头索引节点之前
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
//新头索引向后
newStartVnode = newCh[++newStartIdx];
}
}
}
//当旧头索引大于旧尾索引时,代表旧节点组已遍历完,将盈余的新Vnode增加到末了一个新节点的位置后
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);
}
}
至此,snabbdom的主要功用就剖析完了