什么是虚拟Dom, 什么是dom diff?
什么是虚拟(Virtual)Dom
虚拟dom其实就是JavaScript对象将真实Dom抽象,对象属性描述节点信息,这个JavaScript对象描述了Dom树的结构,当然并不是真实的Dom树,所以称为虚拟Dom。
举个🌰
真实Dom
1 2 3 4 5 6 7 8 9
| <div class="container"> <h1>hello</h1> <p> 阳光正好,微风不燥 </p> <p> 去见你想见的人吧 </p> </div>
|
虚拟Dom
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Vnode = { sel: "div", props: {class: "container"}, children: [ { sel: "h1", props: {}, children: "hello" }, { sel: "p", props: {}, children: "阳光正好,微风不燥" }, { sel: "p", props: {}, children: "去见你想见的人吧" } ] }
|
虚拟(Virtual) Dom Diff
当虚拟dom中的某个或某些节点改变,会产生新的虚拟dom, 新、旧两个虚拟dom树中差异化的最优解映射到真实dom,即为虚拟Dom Diff.
newVnode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| newVnode = { sel: "div", props: {class: "container"}, children: [ { sel: "h1", props: {}, children: "hi" }, { sel: "p", props: {}, children: "阳光正好,微风不燥" }, { sel: "p", props: {}, children: "去见你想见的人吧" } ] }
|
如图解:
如何使用虚拟dom及学习其diff原理
这里借助现有的虚拟dom库来使用、了解、及学习。
这次选择的是vue2.x使用的虚拟dom库——snabbdom.js
核心代码
Diff 算法原理
新旧虚拟dom只会在同层级之间进行比较,不会跨层级进行比较。
Diff 对比流程
当数据改变,产生新的虚拟dom(newVnode),此时调用pathch
方法,将新旧虚拟dom进行diff, 将结果映射到真实dom上,进而更新视图。
具体分析
这是学习snabbdom.js源码后,自己实现简化版的虚拟dom, 链接,感兴趣的可以了解一下.
patch
patch是diff的入口,主要作用就是判断新旧虚拟dom是否为同一节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| export default function(oldVnode, newVnode) { if (!oldVnode.sel) { oldVnode = vnode( oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode ); } if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) { console.log('patch-新旧节点为同一节点,进行pathVnode,进一步比较'); patchVnode(oldVnode, newVnode); } else { console.log('patch-新旧节点不相同,则直接将新节点替换旧节点');
const newDomNode = createElement(newVnode); if (newDomNode && oldVnode.elm) { oldVnode.elm.parentNode.insertBefore(newDomNode, oldVnode.elm); oldVnode.elm.parentNode.removeChild(oldVnode.elm); } } }
|
patchVnode
patchVnode主要作用是判断新旧虚拟dom子集的情况.根据不同的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const checkChildrenEmpty = (Vnode) => { return !Array.isArray(Vnode.children) || Vnode.children.length === 0; } export default function patchVnode(oldVnode, newVnode) {
if (newVnode.text) { if (!checkChildrenEmpty(oldVnode)) { oldVnode.elm.innerHtml = ""; oldVnode.elm.innerText = newVnode.text; } else if (newVnode.text !== oldVnode.text) { oldVnode.elm.innerText = newVnode.text; } } else { if (checkChildrenEmpty(oldVnode)) { for (let i = 0; i < newVnode.children.length; i++) { const ch = createElement(newVnode.children[i]); oldVnode.elm.appendChild(ch); } } else { updateChildren(oldVnode.elm, oldVnode.children, newVnode.children); } } }
|
updateChildren
updateChildren
是patchVnode
中最复杂,最重要的一个环节,新旧虚拟dom子节点的比较就发生在这里。
对比方式:
新旧虚拟dom,每组 首尾
两个指针(start, end),按照5中不同的情况依次比较,通过指针的移动,节点的比较,进而更新视图.
🌰
模版
1 2 3 4 5 6 7 8 9 10 11 12 13
| <- oldVnode -> <ul> <li>a</li> <li>b</li> <li>c</li> </ul> <- newVnode -> <ul> <li>d</li> <li>c</li> <li>b</li> <li>e</li> </ul>
|
首尾双指针
在虚拟dom的体现
算法覆盖情况如下:
1.oldStart 和 newStart,如果sameVnode为true
,进一步patchVnode
,移动指针
2.oldEnd 和 newEnd,如果sameVnode为true
,进一步patchVnode
,移动指针
3.oldStart 和 newEnd,如果sameVnode为true
,进一步patchVnode
,移动指针
4.oldEnd 和 newStart,如果sameVnode为true
,进一步patchVnode
,移动指针
5.以上4种若都不匹配,则把所有剩余旧子节点按照key做了一个Map表,然后用新vnode的key去查找是否有可复用的位置.
6.如果新旧虚拟dom某一组先移动完成(start > end), 则需要安情况插入或删除指定节点
按照上述示例,图解
1.符合第5种情况,此时要将新Vnode中的startVnode,插入旧Vnode中startVnode的前面。注意:此操作是真实dom更新
此时真实dom和指针移动情况
2.符合条件4,此时要操作真实dom,将oldEndVnode对应的节点插在oldStartVnode对应的节点前。
此时真实Dom和指针移动情况
3.同步骤2,依旧符合条件4.
此时真实Dom情况和指针移动情况如下
4.此时情况同上述步骤1,符合条件5.
此时真实Dom情况和指针移动情况
5.此时newStart > newEnd, 表示newVnode遍历完成,oldVnode 剩余节点需要移除.
最终效果:
updateChildren源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
|
export default function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0, oldEndIdx = oldCh.length - 1, newStartIdx = 0, newEndIdx = newCh.length - 1; let oldStartVnode = oldCh[0], oldEndVnode = oldCh[oldEndIdx], newStartVnode = newCh[0], newEndVnode = newCh[newEndIdx];
let oldKeyToIdx let idxInOld let elmToMove while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode === null || oldCh[oldStartIdx] === undefined) { oldStartVnode = oldCh[++oldStartIdx] } else if (oldEndVnode === null || oldCh[oldEndIdx] === undefined) { oldEndVnode = oldCh[--oldEndIdx] } else if (newStartVnode === null || newCh[newStartIdx] === undefined) { newStartVnode = newCh[++newStartIdx] } else if (newEndVnode === null || newCh[newEndIdx] === undefined) { newEndVnode = newCh[--newEndIdx] } else if (checkSameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (checkSameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode); parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (checkSameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode); parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } idxInOld = oldKeyToIdx[newStartVnode.key]; if (!idxInOld) { const dom = createElement(newStartVnode); parentElm.insertBefore(dom, oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { const dom = createElement(newStartVnode); parentElm.insertBefore(dom, oldStartVnode.elm); } else { patchVnode(elmToMove, newStartVnode); oldCh[idxInOld] = null; parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { let base = oldEndVnode.elm; for (; newStartIdx <= newEndIdx; ++newStartIdx) { parentElm.insertBefore(createElement(newCh[newStartIdx]), base.nextSibling); base = base.nextSibling; } } else if (newStartIdx > newEndIdx) { for (;oldStartIdx <= oldEndIdx; ++oldStartIdx) { oldCh[oldStartIdx] && parentElm.removeChild(oldCh[oldStartIdx].elm); } } }
|
参考链接
- snabbdom源码下载
- https://juejin.cn/post/6844903607913938951#heading-3
- https://juejin.cn/post/6994959998283907102