1. それは何ですか?
diff アルゴリズムは、同じレベルのツリー ノードを比較するための効率的なアルゴリズムで、次の 2 つの特徴があります。
- 比較は同じレベルでのみ実行され、クロスレベルの比較は実行されません
- 差分比較の過程で、ループは両側から中央まで比較します
diff
このアルゴリズムは多くのシナリオで適用され、 実際のノードに仮想的にレンダリングされた 古いノードと新しい ノード を比較するためにvue
使用されます dom
dom
VNode
2. 比較方法
diff
全体的な戦略は次のとおりです。深さ優先、同じレイヤーの比較
- 比較は同じレベルでのみ実行され、クロスレベルの比較は実行されません
- 比較中、ループは両側から中央に向かって閉じられます
vue
アルゴリズムの更新の例を次に示しますdiff
。
古いノードと新しいVNode
ノードを次の図に示します。
最初のサイクルの後、古いノード D が新しいノード D と同じであることがわかり、古いノード D は最初のdiff
実ノードとして直接再利用され、古いノードはendIndex
C に移動され、新しいノードは startIndex
Cに移動しました
2周目以降は旧ノードの終点も新ノードの始点と同じ(どちらもC)同様に、diff
最初に作成したBノードの後ろに後から作成したCの実ノードを挿入する。同時に、古いノードは endIndex
B に移動し、新しいノードは startIndex
E に移動します。
3 サイクル目で E が見つからないことがわかり、このとき、新しい実ノード E を直接作成して、2 回目に作成した C ノードの後に挿入するしかありません。同時に、新しいノードは startIndex
A に移動します。古いノードの startIndex
合計 はendIndex
変更されません
4 番目のサイクルで、古いノードと新しいノード (両方とも A) の先頭が同じであることが判明したため、 diff
A の実ノードが作成され、前回作成された E ノードの後ろに挿入されます。同時に、古いノードは startIndex
B に移動し、新しいノードは startIndex
B に移動します。
5周目は4周目と同じ状況なので、 diff
前回作成したAノードの後ろに、後から作成したBリアルノードを挿入します。同時に、古いノードは startIndex
C に移動し、新しいノードの startIndex は F に移動しました。
新しいノードは、 作成する必要があるよりも startIndex
すでに大きく 、 その間のすべてのノード、つまりノード F は、F ノードに対応する実際のノードを直接作成し、それを B ノードの後ろに配置します。endIndex
newStartIdx
newEndIdx
3. 原理分析
データが変更されると、set
メソッドが呼び出されてDep.notify
すべてのサブスクライバーに通知されWatcher
、サブスクライバーは呼び出してpatch
実際のDOM
データにパッチを適用し、対応するビューを更新します
ソースの場所: src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁及旧节点,根据新节点生成dom元素
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
patch
関数の最初の 2 つのパラメーターはoldVnode
sum でVnode
、それぞれ新しいノードと古いノードを表します。主に次の 4 つの判断が行われます。
- 新しいノードはありません。古いノードの
destory
フックを直接トリガーします - 古いノードがないということは、ページが初期化されたばかりのときです.このとき、比較する必要はまったくなく、すべて新しく作成されているため、呼び出すだけです.
createElm
- 古いノードは新しいノード自体と同じであり、
sameVnode
ノードが同じであるかどうかを判断して、同じである場合は、patchVnode
2 つのノードを処理するために直接呼び出します - 古いノードは新しいノード自体とは異なります。2 つのノードが異なる場合は、新しいノードを直接作成し、古いノードを削除します。
以下、主にpatchVnode
その部分について
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
const elm = vnode.elm = oldVnode.elm
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新旧都是静态节点,并且具有相同的key
// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
// 也不用再有其他操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果vnode不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新vnode和老vnode是文本节点或注释节点
// 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnode
主にいくつかの判断を行いました:
- 新しいノードがテキスト ノードであるかどうか。テキスト ノードである場合、
dom
直接更新のテキスト コンテンツは新しいノードのテキスト コンテンツです。 - 新しいノードと古いノードの両方に子ノードがある場合は、子ノードを比較して更新します
- 新しいノードだけに子ノードがあり、古いノードにはありません。したがって、比較する必要はありません。すべてのノードは真新しいため、すべての新しいノードを直接作成するだけです。新しい作成とは、すべての新しいノードを作成し、それらを親
DOM
ノード - 古いノードだけに子ノードがありますが、新しいノードにはありません。つまり、更新されたページでは古いノードがすべてなくなっているので、古いノードをすべて削除する、つまり直接削除する必要があります
DOM
。
子ノードがまったく同じでない場合は、呼び出しますupdateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的key的新节点是否是同一个节点
//不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
while
ループは、主に次の 5 つのシナリオを処理します。
- 古いノードと新しい
VNode
ノードstart
が同じ場合、直接 、patchVnode
古いノードと新しい ノードVNode
の開始インデックスが両方とも 1 増加します。 - 古いノードと新しい
VNode
ノードend
が同じ場合、それも直接であり 、patchVnode
古いノードと新しい ノードVNode
の終了インデックスは両方とも 1 減少します。 - 古い
VNode
ノードがstart
新しいVNode
ノードend
と同じ 場合、この時点で現在の実 ノード を後ろに移動するpatchVnode
必要があり 、同時に、古い ノードの開始インデックスが 1 増加し、 その終了インデックスが新しいノードは 1 減ります。dom
oldEndVnode
VNode
VNode
- 古い
VNode
ノード がend
新しいVNode
ノード と同じ場合start
、この時点で 、現在の実ノードを 前 に移動するpatchVnode
必要があり 、 古いノードの終了インデックスは 1 減り、 新しいノードの開始インデックスは1増加dom
oldStartVnode
VNode
VNode
- 上記の 4 つの状況のいずれにも該当しない場合は、再利用できる同一のノードがないことを意味し、次の 2 つの状況に分けられます。
- 古い 値を値とし、対応するシーケンスを値として
VNode
、 ハッシュ テーブル から同じ古い ノード を見つけて 続行し、同時にこの実数を 対応する実数 の前に移動 します。key
index
value
newStartVnode
key
VNode
patchVnode
dom
oldStartVnode
dom
- 呼び出して
createElm
新しいdom
ノードを作成し、現在のnewStartIdx
位置に配置します
- 古い 値を値とし、対応するシーケンスを値として
まとめ
- データが変更されると、サブスクライバーは実際のパッチ
watcher
を呼び出しますpatch
DOM
- 判断して
isSameVnode
、同じならpatchVnode
メソッドを呼ぶ patchVnode
次のことを行いました:dom
と呼ばれる対応する真実を見つけるel
- 両方にテキスト ノードがあり、等しくない場合は、
el
テキスト ノードをVnode
テキスト ノードに設定します。 oldVnode
子ノードがあり、VNode
子ノードがない場合は、el
子ノードを削除しますoldVnode
子ノードがない場合VNode
、VNode
子ノードが実現され、に追加されますel
- 両方に子ノードがある場合は、
updateChildren
関数比較子ノードを実行します
updateChildren
主に以下の操作を行いました。- 新旧の
VNode
ヘッド ポインターとテール ポインターを設定する - 新旧の頭と尾のポインターを比較し、ループで真ん中に移動し、状況に応じてプロセスを繰り返す呼び出し、
patchVnode
新しいノードを作成するpatch
呼び出し、ハッシュ テーブルから一致する ノードを見つけて、状況を分割する運用へcreateElem
key
VNode
- 新旧の