1.概要
前の章ではdiffアルゴリズムの原理を説明しましたが、この章では、vueがこのアルゴリズムを通じてパッチプロセスを実装する方法を見てみましょう。
第6章でvm._updateについて説明したときに、2つのブランチプロセス(domの最初のレンダリングとそれに続くdomの更新)を含むVnodeを実際のdomに変換する責任があったことを覚えておいてください。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
...
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
...
}
第6章では、最初にdomレンダリングのプロセスを学びました。この章では、2番目の分岐プロセスに進みます。
第二に、パッチ
次のdom updateの例をソースコードと組み合わせて分析します。
<div id="app">
<button v-on:click="changeMsg">设置</button>
<ul>
<li v-for="item in items" :id="item.id">
</li>
</ul>
</div>
var vm = new Vue({
el:"#app",
data:{
msg:'this is msg',
items:[
{id:1},
{id:2},
{id:3}
],
methods:{
changeMsg(){
this.items.push({id:4});
}
}
})
この例は、次のようにdomレンダリングが完了した後は比較的単純です。
「設定」ボタンをクリックすると、追加の列が配列に追加されます。以前の知識から、項目属性値の変更により、関連するサブスクリプションウォッチャーオブジェクトの更新がトリガーされることがわかります。この属性はテンプレートで使用されるため、レンダーワッチャーの更新をトリガーし、vm._render()を実行して、新しいvnodeを生成します。vm。$ el = vm .__ patch __(prevVnode、vnode)、新しいvnodeと古いvnodeの比較を通して、実際のdomを更新します。最終的なレンダリングは次のとおりです。
プロセス全体では、1つのliノードが追加され、その他は変更されません。実際のDOM更新では、グローバルリフレッシュの代わりに新しいliノードのみが追加されるため、効率が最高になると予想されます。
vm .__ patch __(prevVnode、vnode)メソッドは、最終的にsrc / core / vdom / patch.jsで定義されます。
return function patch (oldVnode, vnode, hydrating, removeOnly) {
....
//1、oldVnode为空(首次渲染,或者组件),直接创建新的root element
if (isUndef(oldVnode)) {//
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
//2、oldVnode存在,进入patchVnode过程
const isRealElement = isDef(oldVnode.nodeType)//oldVnode是id为app的dom节点
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
......
//3、创建节点
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
...
}
patchVnodeプロセスでは、2番目のステップを直接調べ、最初にisRealElementとsameVnodeを決定します。isRealElementは、それが実際のdomノードであるかどうかを示し、sameVnodeメソッドを調べます。
function sameVnode (a, b) {
return (
a.key === b.key && (//key相同,undefined也算相同
(
a.tag === b.tag &&//tag相同
a.isComment === b.isComment &&//是否是comment
isDef(a.data) === isDef(b.data) &&//data,属性已定义
sameInputType(a, b)//input相同
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
sameVnodeは実際には2つの記述されたノードの比較可能性であり、満たされる必要があります(後者または条件は一時的に無視されます)。
(1)キーは同じで、undefinedも同じです。
(2)div、ulなどの同じタグ
(3)データ属性。一貫性のある属性定義があるかどうか。たとえば、liノードのid属性。(注:data属性は同じである必要はありません)
(4)入力タイプ。データ型が一貫している必要があるかどうかにかかわらず、タイプタイプは同じです。
つまり、上記の条件が満たされている場合は、ノード間でパッチを適用して子ノードの比較を続行できます。そうでない場合は、elseブランチに直接移動して、新しいノードを作成して挿入します。
ここではキーについて説明します。開発プロセスでv-for、input、その他のタグを使用する場合は、一意の識別子を示すキーを追加する必要があります。そうしないと、要素間に「文字列」が見つかり、取得できません。期待される効果。この例では、キーを追加せず、キーが未定義であるため、比較可能かどうかを判断するために後者の条件のみが採用され、id = "1"のliノードには、「インプレース多重化」が使用されます。時々、これを実行したくない場合があります。これらの2つの要素のキーを異なるように設定する必要があるだけで、これら2つの要素の比較可能性では不十分であり、要素ノードが再作成されます。
この例のルートノードdivはこの条件を満たすため、patchVnodeプロセスに入ります。
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
...
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
//1、对于static节点树,无需比较,直接节点复用
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)
}
//获取oldVnode,Vnode的子节点,进行比较
const oldCh = oldVnode.children
const ch = vnode.children
//2、更新当前节点,执行update相关方法
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)
}
//3、比较子节点
if (isUndef(vnode.text)) {//3.1 非text节点叶节点
if (isDef(oldCh) && isDef(ch)) {//新,旧子节点存在
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {//新子节点存在,旧子节点不村子,添加新节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {//旧子节点存在,新子节点不存在,删除旧节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {//旧节点的为text节点,则设置为空
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {//3.2 text叶节点,但是text不同,直接更新
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
patchVnodeのコアプロセスは3つの部分に分かれています。
1.静的かどうかを判断するコンパイルセクションでは、静的ノードとは何かを紹介しましたが、パッチの効率を向上させるために、静的ノードの場合は要素を直接再利用します。
2.現在のノードを更新し、関連する更新メソッドを実行します。これらのメソッドは次のとおりで、主にdata属性で定義されたコンテンツを更新します。(sameVnodeはdata属性について同じ理由を必要としません)
updateAttrs(oldVnode、vnode)
updateClass(oldVnode、vnode)
updateDOMListeners(oldVnode、vnode)
updateDOMProps(oldVnode、vnode)
updateStyle(oldVnode、vnode)
update(oldVnode、vnode)
updateDirectives(oldVnode、vnode)
3.子ノードを比較します。
(1)非テキストリーフノードの場合は、引き続き子ノードを比較します。新しい子ノードと古い子ノードが存在する場合、比較を続行するためにupdateChildrenが呼び出されます。新しい子ノードが存在する場合、古い子ノードは存在しません。つまり、子ノードは新しく、addVnodesを呼び出すことによって作成されます。新しい子ノードが存在しない場合は、古い子ノードが存在します。つまり、子ノードを意味します冗長です。ノードを削除するには、removeVnodesを呼び出します。
(2)テキストリーフノードでは、テキストの内容が異なる場合、直接更新されます。
この例では、ul要素の子ノード、新旧のノードすべてにli子ノードがあります。したがって、updateChildrenメソッドを入力します。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let 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, 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
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {// os没有定义,os索引向后移动一位
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {//oe没有定义,oe索引向前移动一位
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {//os==ns,保持节点位置不变,继续比较其子节点,os,ns索引向后移动一位
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {//oe==ne,保持节点位置不变,继续比较其子节点,oe,ne索引向后前移动一位。
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // os==ne,将os节点移动到oe后面,继续比较其子节点,os索引向后移动一位,ne索引向前移动一位
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // oe==ns,将oe移动到os节点前,继续比较其子节点,oe索引向后移动一位,ns向前移动一位。
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {//两组都不相同的情况下
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
//在oldstartIdx与oldEndIdx间,查找与newStartVnode相同(key相同,或者节点元素相同)节点索引位置
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
//没有相同节点,则将newStartVnode插入到oldStartVnode前
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
//key值和节点都都相同
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
//移动到oldStartVnode前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
//相同的key,但节点元素不同,创建一个新的newStartVnode节点,插入到oldStartVnode前。
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
//newStartVnode索引加1
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {//旧节点先遍历完,新增剩余的新节点,并插入到ne+1前位置
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {//新节店先遍历完,删除剩余的老节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
updateChildrenメソッドはdiffメソッドを実現したものであり、前の章の図に従ってコードを読み取ることができます。
新しいノードと古いノードの先頭を初期化し、インデックスを終了します。os(oldStartIdx)、oe(oldEndIdx)、ns(newStartIdx)、ne(newEndIdx)でそれぞれ表されます。
(1)osが定義されておらず、osインデックスが1ビット戻ります。
(2)oeは定義されておらず、oeインデックスは1ビット前に移動します。
(3)os == ns、ノードの位置は変更せず、子ノードの比較を続行します。os、nsインデックスは1ビット後方に移動します。
(4)oe == ne、ノードの位置は変更せず、子ノードの比較を続けます。oeのインデックスは、neが1ビット前後に移動します。
(5)os == ne、子ノードの比較を続行し、osノードをoeの後ろに移動し、osインデックスを1ビット後方に移動し、neインデックスを1ビット前方に移動します。
(6)oe == ns、子ノードの比較を続行し、oeをosノードに移動し、oeインデックスを1ビット後方に移動し、nsを1ビット前方に移動します。
(7)osとoeの間で2つのグループが同じでない場合、newStartVnode(同じキーまたは同じノード要素)と同じノードを見つけます。ノードが存在しない場合は、新しいノードを作成し、osの前に挿入します;ノードが存在する場合は、ノードをosに移動します。nsインデックスは1ビット戻ります。
(8)古いノードが最初にトラバースされ、残りの新しいノードが追加され、ne + 1の前の位置に挿入されます。
(9)新しいノードが最初にトラバースされ、残りの古いノードが削除されます。
この例では、liノード、新しいノードと古いノードの最初の3つは同じで、最後のノードが追加されているため、8番目の実行ケースは期待に応えます。
このうち、3、4、5、6の場合は、patchVnodeを呼び出してノードを更新し、リーフノードに到達するまで子ノードを比較し続けます。
3.まとめ
プロセス全体では、新旧のVnodeとdiffアルゴリズムを比較して、実際のdom操作をマッピングします。