Diff
- diff對比的就是vnode
- 同時因爲dom不多跨級移動,因此對比只在同層級中進行
- vue和react的diff算法大致是同樣的
VNode
{
el: div //對真實的節點的引用,本例中就是document.querySelector('#id.classA')
tagName: 'DIV', //節點的標籤
sel: 'div#v.classA' //節點的選擇器
data: null, // 一個存儲節點屬性的對象,對應節點的el[prop]屬性,例如onclick , style
children: [], //存儲子節點的數組,每一個子節點也是vnode結構
text: null, //若是是文本節點,對應文本節點的textContent,不然爲null
}
{
type: 'div',
props: {
className: 'myDiv',
},
chidren: [
{ type: 'p', props: { value:'1' } },
{ type: 'div', props: { value:'2' } },
{ type: 'span', props: { value:'3' } }
]
}
Vue
- vue的diff是邊比較邊更新的過程
- 更改數據時,觸發試圖更新,會生成一棵新的vdom樹
- 對比同級的vnode,僅當vue認爲兩個vnode值得比較時,纔會繼續對比其子節點,若是認爲不值得比較,會直接刪除舊節點,插入新節點
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode
}
function sameVnode(oldVnode, vnode){
// 兩節點key值相同,而且sel屬性值相同,即認爲兩節點屬同一類型,可進行下一步比較
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
- 針對同類型的節點,進行patch操做,並比較他的子節點
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el //讓vnode.el引用到如今的真實dom,當el修改時,vnode.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)
//對於擁有子節點(二者的子節點不一樣)的兩個節點,調用updateChildren
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
} else if (ch){ //只有新節點有子節點,添加新的子節點
createEle(vnode) //create el's children dom
} else if (oldCh){ //只有舊節點內存在子節點,執行刪除子節點操做
api.removeChildren(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)
}
}
- 概況下,就是分別新建4個指針,指向新舊節點的頭尾,而後經過兩兩比對,看是不是同一個類型的節點
- 而後分狀況,若是新舊頭節點是同一個類型節點,直接調用patchVnode便可,同理新舊尾節點是同一個類型節點,也是patchVnode處理
- 但若是判斷新頭和舊尾是同一個類型節點,除了要patchVnode處理外,還須要將尾部節點移到頭部
- 同理,若是新尾和舊頭是同一個類型節點,patchVnode處理後,將頭節點移到尾部
- 若是四種比較都沒有擊中,這時判斷新頭的節點是否在仍未被對比的舊節點中,若是在,patchVnode處理,並在dom中將舊節點移到新頭的位置,本來舊節點對應的Vnode置空,這樣下次判斷就會被跳過,而若是新節點不在舊節點列表裏,則直接新建節點插入便可
- 最後,當頭指針和尾指針相遇並錯事後,判斷當前四個指針的位置,若是舊的頭尾指針中間還有節點,那這些節點都是多餘的,須要被移除,若是新的頭尾指針中間還有節點,那這些節點都是新增的,須要被插入
React
- react的diff是先比較,後更新的流程,對比階段會先記錄下差別
function diff (oldTree, newTree) {
// 當前節點的標誌,之後每遍歷到一個節點,加1
var index = 0
var patches = {} // 用來記錄每一個節點差別的對象
dfsWalk(oldTree, newTree, index, patches)
return patches
}
// 對兩棵樹進行深度優先遍歷
function dfsWalk (oldNode, newNode, index, patches) {
// 對比oldNode和newNode的不一樣,記錄下來,省略了代碼...
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
- 先對比當前節點,記錄差別,而後對比子節點
- 子節點的對比直接從左節點開始,判斷其是否在舊節點列表中,若是不在,則記錄新建,若是在,則標記移動
- 最後根據差別統一更新dom
區別
- vue的diff是邊比較邊更新的,react則是先記錄差別,再統一更新
- vue認爲兩個節點元素是同一個元素是根據key以及sel來判斷的,若是元素類型相同,但classname不一致的話也會認爲是不一樣的元素,而react是根據元素類型以及key來判斷的,因此會認爲是同一個元素類型,直接在這上面進行屬性增減
- diff策略也不同,vue是新舊頭尾四個節點對比,react則是從左到右進行對比
- 另外react16提出了fiber這個概念,diff是基於fiber樹進行的,且能夠被打斷,讓瀏覽器能處理更高優先級的任務,而vue的diff和patch是不能被打斷的
key的做用
- vue和react的key做用是同樣的,就是告訴引擎當前對比的兩個節點是不是同一個節點
- vue沒有key的時候,兩個都是undefined,則vue會認爲其是相同節點,而就地複用這個元素,但極可能這是個input元素,你已經在裏面輸入了某些信息,而更新時不會移動這些信息致使最終結果可能不是你所指望的,因此須要指明key來防止元素的複用
- react則會默認賦值給key,因此子元素中,新舊節點的位置可能發生了移動,但由於react默認index做爲key,仍是會認爲相同位置上的元素是同一個元素,而在其基礎上修改屬性,致使發生和vue同樣的問題