我們知道對於DOM的操作是很慢很耗費資源,拖累性能的,特別是涉及到重排迴流,重新渲染DOM樹是一個性能殺手,所以各大框架都在這個方面做了優化,angularjs是髒值檢測機制,而react首先使用了virtual DOM,vue也使用了這個機制。虛擬dom樹就是用js的對象來儲存和表示dom節點,當發生改變時,通過新舊的虛擬dom的對比(通過diff算法)來做局部的修整,避免整棵dom樹的重新渲染。
真實dom和虛擬dom的表示:
<div>
<p>123</p>
</div>
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};
接下來我們就一起來看一下diff算法是如何比較新舊兩棵樹的差異的。
首先在比較新舊節點時不會跨層級去比較,而是隻比較相同的層級,如下圖所示,因此算法複雜度可以大大減少。
接下來看一下patch打補丁的核心代碼:
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根據Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el) // 移除以前的舊元素節點
oldVnode = null
}
}
//......
return vnode
}
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 標籤名
a.isComment === b.isComment && // 是否爲註釋節點
// 是否都定義了data,data包含一些具體信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 當標籤是<input>的時候,type必須相同
)
}
首先進行兩個節點標籤名,key值,type等的比較,若是值得比較的節點,則繼續比較內層節點,若不相同則證明vnode完全不同,直接用vnode代替oldvnode,oldvnode設爲null等待回收。接下來看一下patchVnode是如何比較兩個節點的:
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode)
}else if (oldCh){
api.removeChildren(el)
}
}
}
首先定義真實的dom爲el,如果新舊節點指向對象相同則直接返回,如果文本節點不一樣則將el的文本節點設置爲vnode的文本節點,如果都存在子節點而且不相等,就執行updateChildren函數比較子節點,如果vnode存在子節點而oldvnode不存在,則直接真實化子節點放到el裏,如果vnode不存在子節點而oldvnode存在子節點則移除el下的子節點。
接下來看一下關鍵的當兩者都存在子節點且都不一樣時的updateChildren函數:
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 對於vnode.key的比較,會把oldVnode = 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 {
// 使用key時的比較
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
- 將vnode的子節點vch和oldvnode的子節點oldch提取出來
- oldch和vch各有兩個頭尾的變量startIdx和EndIdx,它們的2個變量相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設置了key,就會用key進行比較,在比較的過程中,變量會往中間靠,一旦startIdx>EndIdx表明oldch和vch至少有一個已經遍歷完了,就會結束比較。
流程圖如下: