Vue源码解析之虚拟DOM和diff算法
举个小栗子:
我们要对一个屋子进行装修,装修图上只是对一部分空间进行改造,现有两种选择:①全拆了再按照装修图进行建造,②根据装修图,只对修改部分进行建造。
关于真实dom和虚拟dom的区别大致和上述例子相同,真实dom是①每一次修改都需要拆了之前的再新建,而虚拟dom是②通过比对不同,通过appendchild等来操作dom节点。
渲染真实dom的开销非常大,和页面性能优化中的重绘重排意义类似。当我们修改了页面中的某个数据,如果直接渲染到真实DOM中会引起整棵数的重绘重排, 如果我们使用虚拟dom,只让我们修改的数据映射到真实DOM, 做一个最少化重绘重排,这样的页面性能优化将大大提高。
知识讲解
虚拟dom
将真实的DOM的数据抽取出来,以对象的形式模拟树形结构
1 2 3 4 5 6 7 8 9 10 11
| <div> <p>你好</p> </div>
const Vnode = { tag: 'div', children: [ { tag: 'p', text: '你好' } ] };
|
1. h函数用来产生虚拟节点
1 2 3 4 5 6 7
| const hFun = h('a', { props:{ href: 'http:baidu.com' } }, '百度');
{'sel': 'a','data': { props: { href: 'http:baidu.com' } }, 'text': '百度'};
<a href = "http:baidu.com" >百度</a>
|
2. 虚拟节点的属性
1 2 3 4 5 6 7 8
| { children: undefined data: {} elm: undefined key: sel: "" text: "" }
|
3. h函数使用patch函数进行”上树“
创建节点时,所有子节点都需要递归创建的
diff算法
简单的说:就是新旧虚拟dom 的比较,如果有差异就以新的为准,然后再插入的真实的dom中,重新渲染。
1. 在diff中,key起着非常重要的作用
- 无key:新的和旧的只会在进行头尾两端比较
- 有key:除了比较头尾两端,还会根据key生成对象
oldKeyToIdx
中查找匹配的节点
为节点设置key可以更高效的利用dom
例如:
在B和C之间加一个F,Diff算法默认执行起来是这样的:C更新成F,D更新成C,E更新成D,最后再插入E,这个效率emmm。。。。。。
如果我们给每个节点设置好key,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
哦豁,for循环要设置key…..
2. 特点
- 只会做同级比较,不做跨级比较
- 比较后几种情况
if (oldVnode === vnode)
,他们的引用一致,可以认为没有变化。
if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text)
,文本节点的比较,需要修改,则会调用Node.textContent = vnode.text
。
if( oldCh && ch && oldCh !== ch )
, 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren
函数比较子节点,这是diff的核心
else if (ch)
,只有新的节点有子节点,调用createEle(vnode)
,vnode.el
已经引用了老的dom节点,createEle
函数会在老dom节点上添加子节点。
else if (oldCh)
,新节点没有子节点,老节点有子节点,直接删除老节点。
3. 总结
4. 在实际开发中要注意
- 尽量不要跨层级的修改dom
- 在开发组件时,保持稳定的 DOM 结构会有助于性能的提升
- 设置key可以让diff更高效
手写源码
虚拟DON
1 2 3 4 5 6 7
| export default function(sel, data, children, text, elm){ const key = data.key return { sel, data, children, text, elm, key } }
|
patch第一次上树
精细化比较具体内容
patch.js
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
| import vnode from './vnode.js'; import createElement from './createElement.js'; import patchVnode from './patchVnode.js';
export default function patch (oldVnode, newVnode) { if(oldVnode.sel == '' || oldVnode.sel == undefined){ oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode); } if(oldVnode.key==newVnode.key && oldVnode.sel == newVnode.sel) { console.log("是同一个节点"); patchVnode(oldVnode, newVnode) } else { console.log("不是同一个节点,暴力插入新的,删除旧的"); let newVnodeElm = createElement(newVnode); if(oldVnode.elm.parentNode && newVnodeElm) { oldNode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm); } oldVnode.elm.parentNode.remobeChild(oldVnode.elm) } }
|
createElement.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export default function createElement (vnode) { let demNode = document.createElement(vnode.sel); if(vnode.text != '' && vnode.children == undefined || vnode.children.length == 0) { domNode.innerText = vnode.text } else if(Array.isArray(vnode.children) && vnode.children.length > 0) { for (let i = 0; i < vnode.children.length; i++) { let ch = vnode.children[i]; let chDOM = createElement(ch); domNode.appendChild(chDOM); } } vnode.elm = domNode; return vnode.elm; }
|
patchVnode.js
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
| import createElement from "./createElement.js"
export default function patchVnode(oldVnode, newVnode) { if (newVnode === oldVnode) return; if ( newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0) ) { console.log("新vnode有text属性"); if (newVnode.text != oldVnode.text) { oldVnode.elm.innerText = newVnode.text; } } else { console.log("新vnode没有text属性"); if (oldVnode.children != undefined && oldVnode.children.length > 0) {
let un = 0; for (let i = 0; i < newVnode.children.length; i++) { let ch = newVnode.children[i]; let isExist = false; for(let j = 0; j < oldVnode.children.length; j++) { if(oldVnode.children[j].sel == ch.sel && oldVnode.children[j].key == ch.key){ isExist = true; } } if(!isExist) { let dom = createElement(ch); ch.elm = dom; if (un < oldVnode.children.length) { oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm); } else { oldVnode.elm.appendChild(dom); } } else { un++; } }
} else { oldVnode.elm.innerHTML = ""; for (let i = 0; i < newVnode.children.length; i++) { let dom = createElement(newVnode.children[i]); oldVnode.elm.appendChild(dom); } } } }
|
diff算法优化
子节点更新策略
四种命中查找
- 新前与旧前
- 新后与旧后
- 新后与旧前(新前指向的节点,移动到旧后之后)
- 新前与旧后(新前指向的节点,移动到旧前之前)
命中一种就不再进行命中判断,若全都没命中,需要用循环来寻找
举例子
新增:
刚开始,旧前和新前均指向第一个节点,对比这两者是否相同,若相同,同时移动一个节点,再次比较,直到旧前指针移动到没有节点处,新前与新后之间的所有节点为新节点,全部插入到dom树中。
删除:
刚开始,旧前和新前均指向第一个节点,对比这两者是否相同,若相同,同时移动一个节点,再次比较……
如果旧前和新前指向的节点不相同,使用下一个命中查找:新后与旧后,此时新后与旧后同时移动一个节点,进行比较,直到新节点循环完毕,此时若老节点有剩余节点,说明他们是要被删除的节点
代码改进
patchVnode.js
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
| import createElement from "./createElement.js" import updateChildren from "./updateChildren.js"
export default function patchVnode(oldVnode, newVnode) { if (newVnode === oldVnode) return; if ( newVnode.text != undefined && (newVnode.children == undefined || newVnode.children.length == 0) ) { if (newVnode.text != oldVnode.text) { oldVnode.elm.innerText = newVnode.text; } } else { if (oldVnode.children != undefined && oldVnode.children.length > 0) { updateChildren(oldVnode.elm, oldVnode.children, newVnode.children); } else { oldVnode.elm.innerHTML = ""; for (let i = 0; i < newVnode.children.length; i++) { let dom = createElement(newVnode.children[i]); oldVnode.elm.appendChild(dom); } } } }
|
updateChildren.js
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
| import patchVnode from "./patchVnode.js" import createElement from "./createElement.js"
function checkSameVnode(a, b) { return a.sel == b.sel && a.key == b.key; } export default function updateChildren(parentElm, oldCh, newCh) { let oldStartIdx = 0; let newStartIdx = 0; let oldEndIdx = oldCh.length - 1; let newEndIdx = oldCh.length - 1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){ if(oldStartVnode == null || oldCh[oldStartIdx] == undefined) { oldStartVnode = old[++oldStartIdx] } else if(oldEndVnode == null || oldCh[oldEndIdx] == undefined) { oldEndVnode = old[--oldEndIdx] } else if(newStartVnode == null || newCh[newStartIdx] == undefined) { newStartVnode = old[++oldStartIdx] } else if(newEndVnode == null || newCh[newEndIdx] == undefined) { newEndVnode = old[--newEndIdx] } else if(checkSameVnode(oldStartVnode, newStartVnode)){ patchVnode(oldStartVnode,newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if(checkSameVnode(oldStartVnode, newStartVnode)){ patchVnode(oldStartVnode, newStartVnode); 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(!keyMap) { keyMap = {}; for (let i = oldStartIdx; i <= oldEndIdx; i++) { const key = oldCh[i].key; if (key != undefined) { keyMap[key] = i; } } } const idxInOld = keyMap[newStartVnode.key]; if(idxInOld == undefined) { parentElm.insertBefore(ceateElem(newSrartVnode), oldStartVnode.elm); } else { const elmToMove = oldCh[idxInOld]; patchVnode(elmToMove, newStartVnode); oldCh[idxInOld] = undefined; parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx] } } if (newStartIdx <= oldEndIdx) { for(let i = newStartIdx; i <= newEndIdx; i++) { parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm); } } else if (oldStartIdx <= oldEndIdx) { for (let i = oldStartIdx; i <= oldEndIdx; i++) { if (oldCh[i]) { parentElm.removeChild(oldCh[i].elm); } } } }
|
参考: