Open HolyZheng opened 6 years ago
作者:holyZheng 转载请注明出处
此篇结合我最近造的小轮子hoz进行分析,其中的virtual dom算法主要参考snabbdom库。 virtual dom 什么是virtual dom virtual dom的本质是一个用来映射真实dom的JavaScript对象,比如 // hoz库中的 src/vdom/vnode.js
此篇结合我最近造的小轮子hoz进行分析,其中的virtual dom算法主要参考snabbdom库。
virtual dom的本质是一个用来映射真实dom的JavaScript对象,比如
// hoz库中的 src/vdom/vnode.js
class VNode { constructor (sel, tagName, id, className, el, children, data, text, key) { this.tagName = tagName // DIV this.sel = sel // div#id.class this.id = id // id this.className = className // [] this.children = children // [] this.el = el // node this.data = data // {} this.key = key this.text = text } }
通过一个vnode对象去对应一个dom元素,vnode的属性对应反映dom元素的属性。然后我们可以定义一个toVnode方法,把一个dom tree转为virtual dom tree。 ```js // hoz库中 src/vdom/toVnode.js import VNode from './vnode' import * as utils from './is' function toVnode (node, utilsTool) { const api = (utilsTool === undefined ? utils : utilsTool) // 自定义的一些工具 let text // 判断是否为node节点,不是抛出错误 if (!node) { throw new Error('Please make sure you have pass the argument "node" in to function toVnode') } // 如果是element类型节点 if (api.isElement(node)) { // 收集该节点各属性信息,这里实现方式比较多,只要获取到需要的足够的信息就行了 // 这里获取了id,类名cn,类名数组ca,类字符串如 .classA.classB,sel 等等 const id = node.id ? node.id : '' const cn = node.getAttribute('class') const ca = cn ? cn.split(' ') : null // classArray const c = cn ? '.' + cn.split(' ').join('.') : '' // .classA.classB const sel = node.tagName.toLowerCase() + '#' + id + c const attrs = {} const elmAttrs = node.attributes const elmChildren = node.childNodes const children = elmChildren.length ? [] : null const tag = node.tagName let i, n for (i = 0, n = elmAttrs.length; i < n; i++) { if (elmAttrs[i].nodeName !== 'class' && elmAttrs[i].nodeName !== 'id') { // id 和 class例外处理 attrs[elmAttrs[i].nodeName] = elmAttrs[i].nodeValue } } // 如果给节点指定了key属性,则获取key的值 const key = attrs.key ? attrs.key : undefined // 如果有子节点,对子节点递归调用toVnode方法 for (i = 0, n = elmChildren.length; i < n; i++) { children.push(toVnode(elmChildren[i], api)) } return new VNode(sel, tag, id, ca, node, children, attrs, undefined, key) // 文本节点 } else if (api.isText(node)) { text = node.textContent return new VNode(undefined, undefined, undefined, undefined, node, undefined, undefined, text, undefined) // 注释节点 } else if (api.isComment(node)) { text = node.textContent return new VNode('commont', undefined, undefined, undefined, node, undefined, undefined, text, undefined) } else { return new VNode('', undefined, undefined, undefined, node, undefined, undefined, undefined, undefined) } } export default toVnode
toVnode方法说白了就是,判断当前节点类型,创建对应类型的vnode,然后如果有子节点的话,对子节点递归调用toVnode方法,将子节点也转为vnode,执行结束后,我们就可以得到一棵以传入节点为根节点的virtual dom tree了。
到这里我们已经有了一个映射真实dom的virtual dom类(vnode),以及一个将dom转为virtual dom方法(toVnode)
接下来到了关键的diff算法,diff算法就是用来对比新旧两个vnode,计算出最小的修改单位,去操作更新真实的dom。下面是一张解释diff流程的经典图,我相信你不是第一次看到 diff会算法会在同一层比较新旧两个vnode,从而将算法复杂度降低到O(n)。hoz库中diff算法的实现在patch.js文件中,这里面有几个关键的函数
/** * 比较新老节点,相同则进行patchVnode,不同的话在真实dom上 * 用新的节点,取代旧的节点。 */ export default function patch (oldVnode, vnode) { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oldElement = oldVnode.el let parentElement = api.parentNode(oldElement) createElm(vnode) if (parentElement !== null) { api.insertBefore(parentElement, vnode.el, api.nextSibling(oldElement)) api.removeChild(parentElement, oldVnode.el) } } return vnode }
第一个关键函数就是我们diff算法的入口函数patch方法,该方法负责,判断oldVnode,和vnode是否为同一节点,如果是,调用patchVnode方法,进一步比较处理,不是的话,用新节点代替就节点。
ps:通过sameVnode进行判断
function sameVnode (oldVnode, vnode) { return vnode.key === oldVnode.key && vnode.tagName === oldVnode.tagName // 这里可按自己需要进行比较 }
上面提到,如果是同一节点的话,就会进入到patchVnode方法中
function patchVnode (oldVnode, vnode) { const element = vnode.el = oldVnode.el let oldCh = oldVnode.children let ch = vnode.children // 如果相同,不需要比较 if (oldVnode === vnode) { console.log('oldVnode === vnode') return } if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { api.setTextContent(element, vnode.text) } else { // 更新element的className, id, 和其他属性 updateElm(element, vnode) if (oldCh && ch && oldCh !== ch) { updateChildren(element, oldCh, ch) } else if (ch) { // 新vnode有子节点,oldvnode没有子节点,给element添加子节点 addVnodes(element, null, ch, 0, ch.length - 1) } else if (oldCh) { // 新vnode没有子节点,oldvnode有子节点,给element删除子节点 removeVnodes(element, oldCh, 0, oldCh.length - 1) } } }
patchVnode方法的任务就是
刚刚说到,如果它们都有子节点,那么会调用updateChildren去继续比较它们的子节点,updateChildren是这里的难点,可以说,最复杂的操作就是这一步,代码
function updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let newEndIdx = newCh.length - 1 let oldStartVnode = oldCh[0] let newStartVnode = newCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx let idxInOld let emlToMove let before while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // oldStartIdx 与 oldEndIdx 与 newStartIdx 与 newEndIdx 之间四中比较 // 简单的判断子节点的移位 if (oldStartVnode == null) { oldStartVnode = oldCh[++oldStartIdx] } else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx] } else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx] } else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode) api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIndex(oldCh, oldStartIdx, oldEndIdx) } idxInOld = oldKeyToIdx[newStartVnode.key] // idx 不存在,说明该节点是新增的,原来没有的,故创建一个新节点,并插入 if (!idxInOld) { api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.el) newStartVnode = newCh[++newStartIdx] } else { emlToMove = oldCh[idxInOld] // sel 不同的话,证明也是一个原来没有的节点,所以创建并插入 if (emlToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.el) } else { patchVnode(emlToMove, newStartVnode) oldCh[idxInOld] = null api.insertBefore(parentElm, emlToMove.el, oldStartVnode.el) } newStartVnode = newCh[++newStartIdx] } } } // 这是oldVnode已经遍历完,vnode还没有,说明vnode比oldVnode节点多,所以将多出的节点创建并插入 if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el addVnodes(parentElm, before, newCh, newStartIdx, oldEndIdx) } else { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
下面图反映了updateChildren的运作方式
这就是diff得大致流程了。
好!支持一下
作者:holyZheng 转载请注明出处
class VNode { constructor (sel, tagName, id, className, el, children, data, text, key) { this.tagName = tagName // DIV this.sel = sel // div#id.class this.id = id // id this.className = className // [] this.children = children // [] this.el = el // node this.data = data // {} this.key = key this.text = text } }
toVnode方法说白了就是,判断当前节点类型,创建对应类型的vnode,然后如果有子节点的话,对子节点递归调用toVnode方法,将子节点也转为vnode,执行结束后,我们就可以得到一棵以传入节点为根节点的virtual dom tree了。
到这里我们已经有了一个映射真实dom的virtual dom类(vnode),以及一个将dom转为virtual dom方法(toVnode)
diff算法
接下来到了关键的diff算法,diff算法就是用来对比新旧两个vnode,计算出最小的修改单位,去操作更新真实的dom。下面是一张解释diff流程的经典图,我相信你不是第一次看到 diff会算法会在同一层比较新旧两个vnode,从而将算法复杂度降低到O(n)。hoz库中diff算法的实现在patch.js文件中,这里面有几个关键的函数
patch
第一个关键函数就是我们diff算法的入口函数patch方法,该方法负责,判断oldVnode,和vnode是否为同一节点,如果是,调用patchVnode方法,进一步比较处理,不是的话,用新节点代替就节点。
ps:通过sameVnode进行判断
patchVnode
上面提到,如果是同一节点的话,就会进入到patchVnode方法中
patchVnode方法的任务就是
updateChildren
刚刚说到,如果它们都有子节点,那么会调用updateChildren去继续比较它们的子节点,updateChildren是这里的难点,可以说,最复杂的操作就是这一步,代码
下面图反映了updateChildren的运作方式
这就是diff得大致流程了。