Open maodouchen opened 5 years ago
snabbdom.js里有几个核心的函数 h,createElm, init patchVnode upgradeChildren 。 前几个还是很好理解的,upgradeChildren不太好理解。 他主要是用h函数生成了Vnode。 再调用用patch,diff dom, 计算出最新dom,插入节点。再此过程中有各种钩子函数,这里不详细讲解钩子。 使用方式如下:
let snabbdom = require('snabbdom') var patch = snabbdom.init([ require('snabbdom/modules/class').default, require('snabbdom/modules/props').default, require('snabbdom/modules/style').default, require('snabbdom/modules/eventlisteners').default, ]); // h 函数 入参,对象包括下列属性: sel,data(属性),children,text文本,key。 // 出参 Vnode节点对象 包括下列属性 sel,data(属性),children,text文本,key,elm真实dom var h = require('snabbdom/h').default; var vnode = h( 'ul.className#id', {style: 'xxxx'}, [ h( 'li', {}, '1' ), h( 'li', {}, '2' ), h( 'li', {}, '3' ), ] ); var newVnode = h( 'ul', {style: 'xxxx'}, [ h( 'li', {}, 'a' ), h( 'li', {}, 'b' ), h( 'li', {}, 'c' ), ] ); // diff新旧vnode的区别 计算出最新的dom结构 patch(vnode, newVnode);
h函数
// h函数 入参 sel代表tagName和class和id等,比如传入的sel是 div.container#box(tagName是div class是container id是box), b代表props(data)就是tagName的一些属性, c代表children代表这个节点的子节点 // h函数 出参 return vnode(sel, data, children, text, undefined); 最终返回了一个vnode的节点 // 此函数的作用: 入参b是可传可不传的。 此函数的作用是即使不传b, 也可以判断出 sel和children和text。最后根据这三个参数来调用vnode函数,最终得到vnode节点。SVG tagName特殊处理。 function h(sel, b, c) { var data = {}, children, text, i; // 如果传了children if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } else if (c && c.sel) { children = [c]; } } // 如果不传children 但是传了props else if (b !== undefined) { if (is.array(b)) { children = b; } else if (is.primitive(b)) { text = b; } else if (b && b.sel) { children = [b]; } else { data = b; } } if (is.array(children)) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = vnode_1.vnode(undefined, undefined, undefined, children[i]); } } if (sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && (sel.length === 3 || sel[3] === '.' || sel[3] === '#')) { addNS(data, children, sel); } return vnode(sel, data, children, text, undefined); } // 生成vnode节点 function vnode(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 }; } exports.vnode = vnode;
createElm函数
// 给这个vnode 添加vnode.elm(真实dom)如果sel等于undefined则添加一个文本节点(createTextNode) 如果sel不等于undefined 按照sel中的class id 创建一个element(深度遍历子节点) // 总结下:传入一个Vnode 深度遍历这个vnode后,给vnode.ele = 该Vnode代表的真实的dom节点 function createElm(vnode, insertedVnodeQueue) { var i, data = vnode.data; if (data !== undefined) { if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); data = vnode.data; } } var children = vnode.children, sel = vnode.sel; if (sel === '!') { if (isUndef(vnode.text)) { vnode.text = ''; } vnode.elm = api.createComment(vnode.text); } else if (sel !== undefined) { // 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; var elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag) : api.createElement(tag); if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')); for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); if (is.array(children)) { // 深度遍历 for (i = 0; i < children.length; ++i) { var ch = children[i]; if (ch != null) { api.appendChild(elm, createElm(ch, insertedVnodeQueue)); } } } else if (is.primitive(vnode.text)) { api.appendChild(elm, api.createTextNode(vnode.text)); } i = vnode.data.hook; // Reuse variable if (isDef(i)) { if (i.create) i.create(emptyNode, vnode); if (i.insert) insertedVnodeQueue.push(vnode); } } else { vnode.elm = api.createTextNode(vnode.text); } return vnode.elm; }
init函数
// init函数是snabbdom的核心函数,他返回了一个patch函数, // patch函数的作用 function init(modules, domApi) { // 定义domAPI (createElement insert ......) // modules : require('snabbdom/modules/class').default, // makes it easy to toggle classes // require('snabbdom/modules/props').default, // for setting properties on DOM elements // require('snabbdom/modules/style').default, // handles styling on elements with support for animations // require('snabbdom/modules/eventlisteners').default, // 定义cbs{create: 'class' update: 'class' remove: xxx destory: xxx pre: xxx post: xxx} 就是在声明周期create的时候 要去执行classModule 差不多就是这个意思 var i, j, cbs = {}; var api = domApi !== undefined ? domApi : htmldomapi_1.default; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { var hook = modules[j][hooks[i]]; if (hook !== undefined) { cbs[hooks[i]].push(hook); } } } // return上面定义了若干函数patchVnode updateChildren removeVnodes invokeDestroyHook addVnodes createElm emptyNodeAt等 // snabbdom里面有个很常用的函数是sameVnode函数,顾名思义判断两个Vnode是否相同,如果key和sel相等,就认为两个vnode可能相等。 注意是可能。 // patch函数的作用 如果sameVnode(newVnode, oldVnode)函数返回false,那么则不需要在进一步的对比了,直接在parent里插入newVnode.elm,并且parent里删掉oldVnode.elm // 如果如果sameVnode(newVnode, oldVnode)函数返回true则认为可以进一步对比两个Vnode,调用patchVnode函数 return function patch(oldVnode, vnode) { var i, elm, parent; var insertedVnodeQueue = []; if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 给vnode添加一个ele属性 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); } } return vnode; }; } // 辅助函数 function sameVnode(vnode1, vnode2) { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
patchVnode函数
// snabbdom要求 如果该node有text属性 则不能用children,如果有children,则不能有text。不存在这种dom结构<div>hello world <span>xxx</span></div> // 先赋值 newVnode.elm = oldVnode.elm; 新旧节点的elm相等,根据后面的对比,修改新节点的elm. // 对比两个节点的方式(对比两个节点的顺序): // 1. 如果两个节点的引用相同,则不需要在进行对比。 // 2. newVnode没有text节点 // a. 如果新旧节点都有children,则updateChildren(newVnode, oldVnode) 后面讲这个函数 // b. 如果新节点有children,旧节点没有children, 将newVnode.elm最后面插入dom节点(所有的children生成的dom节点) // c. 如果新节点有没有children,旧节点有children,则将newVnode.elm的所有子节点删除 // d. 如果新旧节点都没有children,则将newVnode.text设置为空 // 3. newVnode有text节点 // a. newVnode.text和oldVnode.text不相等,则重置newVnode.elm的text function patchVnode(oldVnode, vnode, insertedVnodeQueue) { var i, hook; var elm = vnode.elm = oldVnode.elm; var oldCh = oldVnode.children; var ch = vnode.children; if (oldVnode === vnode) return; if (isUndef(vnode.text)) { // 如果child和oldchild都存在的话 并且不相等 就update children if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue); } // 如果child存在 oldchild不存在 并且oldVnode.tex存在那么就给elm设置个空text, 给el 增加子元素 else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); } // 如果oldchild存在 和child都不存在 那么久移除elm下面的 0 到 oldch.length-1 的子元素 else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1); } // oldchild 和child都不存在 并且oldVnode.text存在 则给elm设置个空text else if (isDef(oldVnode.text)) { api.setTextContent(elm, ''); } } // 如果vnode.text不存在 并且oldVnode.text !== vnode.text 则给elm 设置vnode.text 属性 else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text); } } // 辅助函数 // 在parentElm里,before前, 插入子节点(vnode[startIdx~endIdx]) function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) { for (; startIdx <= endIdx; ++startIdx) { var ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); } } }
updateChildren 函数,图示:
// 对比s1s2, e1e2,s1e2,e1s2这四个节点是否sameVnode,将dom节点移动,比如s1 sameVnode e2,说明在dom节点中,s1移动到了e1的后面。 // 如果进行了上述的四次对比,发现都不是sameVnode, 就继续用key值进行对比,oldKeyIdx存放的是所有旧节点的key值,如果s2的key值不在oldKeyIdx中,说明s2是新增节点,在真实dom中,将s2的dom节点放在s1的前面。 // 如果如果s2的key值在oldKeyIdx中,就继续通过tagName来对比两个节点是否相同 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; var idxInOld; var elmToMove; var before; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left } else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx]; } else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx]; } // 如果s1 sameVnode s2说明这两个vnode节点有继续对比的必要 所以执行patchVnode通过children text属性 进一步对比 。s1++, s2++指针往后移 else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } // 如果e1和e2 sameVnode,说明这两个vnode节点有继续对比的必要,所以要执行patchVnode 通过children text属性 进一步对比, e1--, e2--往前移 else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } // 如果s1和e2 sameVnode, 说明这两个vnode节点有继续对比的必要,所以要执行patchVnode 通过children text属性 进一步对比。 s1++ e2-- // 且说明新的dom节点在位置上发生了变化:s1移动到了最后面,所以执行 api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } // 如果e1 s2 sameVnode,说明新的节点位置上发生了变化,e1的dom节点移动到了最前面 else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } // 对比了四次,都不是sameVnode,则继续用key进行对比。将所有旧节点的key存放在oldKeyToIdx中。 else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key]; // 如果s2在oldKeyToIdx中不存在,则说明s2是新增的节点,创建这个新节点的dom,并且将它插在s1的前面。s2++ if (isUndef(idxInOld)) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } // 如果s2在oldKeyToIdx中存在,且记为 elmToMove = oldCh[idxInOld]; 如果s2和elmToMove tagName不同,和上面的处理方式一样,当成新增节点来处理。如果s2和elmToMove tagName相同, // 则通过patchVnode继续对比子节点,不需要在根据s2重复创建一个dom节点,可以直接用elmToMove的dom节点,将它插在s1的前面。 else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx]; } } } // 如果s1 > e1 说明oldChild先遍历完,那么newchild中间没有遍历的就是新增节点,将这些新增节点插入到dom中。 if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } // 如果s2 > e2 说明newChild先遍历完,那么oldChild中间没有遍历的就是要删除的节点,将这些dom节点删掉。 else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
可以总结下各个函数的作用
https://blog.csdn.net/qq_39290323/article/details/108336194
snabbdom.js里有几个核心的函数 h,createElm, init patchVnode upgradeChildren 。 前几个还是很好理解的,upgradeChildren不太好理解。 他主要是用h函数生成了Vnode。 再调用用patch,diff dom, 计算出最新dom,插入节点。再此过程中有各种钩子函数,这里不详细讲解钩子。 使用方式如下:
h函数
createElm函数
init函数
patchVnode函数
updateChildren 函数,图示:
可以总结下各个函数的作用