仮想 dom
および diff
アルゴリズムは、vue
の学習プロセスにおいて難しいため、次の手順で習得する必要があります。インタビュー 豆知識。この 2 つは相互に補完し合い、vue
フレームワークの中核となります。今日は、vue2
の virtual dom
および diff
アルゴリズムを要約します。 (学習ビデオ共有: vue ビデオ チュートリアル)
レンダリング関数
が変換されることはわかっています。 VNode
に。 VNode
は実際には JavaScript
オブジェクトに基づいたツリーであり、オブジェクト属性を使用してノードを記述します。実際、これは実際の DOM
の抽象化レイヤーにすぎません。最後に、一連の操作を通じて、このツリーを実際の環境にマッピングできます。
たとえば、次の template
<template> <span class="demo" v-show="isShow"> This is a span. </span> </template>
がある場合、これを VNode
に置き換えると、将来は次のようになります
{ tag: "span", data: { /* 指令集合数组 */ directives: [ { /* v-show指令 */ rawName: "v-show", expression: "isShow", name: "show", value: true, }, ], /* 静态class */ staticClass: "demo", }, text: undefined, children: [ /* 子节点是一个文本VNode节点 */ { tag: undefined, data: undefined, text: "This is a span.", children: undefined, }, ], };
一般に、VNode
は JavaScript
オブジェクトです。この JavaScript
オブジェクトは、実際の DOM
を完全に表すことができます。
著者は 2 つの理由があると考えています
理由仮想 DOM
これは JavaScript
オブジェクトに基づいており、実際のプラットフォーム環境に依存しないため、ブラウザ プラットフォーム、Weex、Node などの クロスプラットフォーム機能を備えています。
操作を削減しますDOM
。ページ変更の場合、操作の比較には VNode
のみが使用されます。必要なのは最後のマウント更新のみです。 DOM
、頻繁な操作を回避します DOM
、ブラウザのリフローと再描画を減らし、それによって ページのパフォーマンスを向上させます。
コンポーネントの更新 # # に関係する diff アルゴリズム## を見てみましょう。 。
Rendering watcher に渡される
get メソッドは、実際には
updateComponentmethod です。
updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
したがって、コンポーネントは、応答データが変更されたときにこのメソッドを再度トリガーします。次に、
updateComponent の _update メソッドを詳細に分析してみましょう。
は、_update メソッドですが、渡されるパラメータが異なります。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
vm._vnode = vnode
// 初次渲染没有 prevVnode,组件更新才会有
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
// ...
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
__patch__
method
__patch__
patch# を見てみましょう##メソッドは 4 つのパラメータを受け取ります。初期レンダリング中は oldVnodevm.$el であるため、
null であるため、
oldVnode# は存在しません初期レンダリング中。##。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">// src/core/vdom/patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 新节点不存在,只有oldVnode就直接销毁,然后返回
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 没有老节点,直接创建,也就是初始渲染
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 不是真实dom,并且是相同节点走patch
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 这里才会涉及到diff算法
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// ...
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 1.创建一个新节点
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)
)
// 2.更新父节点占位符
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// 3.删除老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
//触发插入钩子
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}</pre><div class="contentsignin">ログイン後にコピー</div></div>
patch
このメソッドのおおよそのプロセスは次のとおりです:
新しいノードはなく、古いノードのみが存在し、古いノードは存在します。直接削除されます。
を入力します。
patchVnode新しいノードと古いノードが両方ある場合は、それらが同じノードであるかどうかを判断し、異なる場合は古いノードを削除し、新しいノードを直接追加します。
// src/core/vdom/patch.js function sameVnode (a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) } function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
2 つの
VNode key
同じ
すべてが同じ非同期コンポーネント ファクトリ関数を持ちます。 function
(コンポーネントのタグ名)現在のノード)
isComment
両方ともコメント ノードであるかどうか
data
(現在のノードに対応するオブジェクトには特定のデータ情報が含まれています (VNodeData タイプです)
ラベルが <input>
の場合、
2 つの
VNode
の
input の場合、タイプ は同じである必要があります。このとき、これら 2 つの
VNode は
sameVnode としてカウントされ、
patchVnode 操作を直接実行できます。
patchVnode
patchVnode
// src/core/vdom/patch.js function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 两个vnode相同则直接返回 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } 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 } /* 如果新旧VNode都是静态的,同时它们的key相同(代表同一节点), 并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次), 那么只需要替换componentInstance即可。 */ 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 /*调用prepatch钩子*/ 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) } // 新节点不是文本节点 if (isUndef(vnode.text)) { /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/ if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) /*如果只有新节点有子节点,先清空elm文本内容,然后为当前DOM节点加入子节点。*/ } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) /*如果只有老节点有子节点,则移除elm所有子节点*/ } else if (isDef(oldCh)) { removeVnodes(oldCh, 0, oldCh.length - 1) /*当新老节点都无子节点的时候,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/ } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 新节点是文本节点,如果文本不一样就设置新的文本 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } /*调用postpatch钩子*/ if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
1. 新旧ノードが同じ場合は、そのままリターンします。
2.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换componentInstance即可。
3.新节点不是文本节点,新老节点均有children
子节点,则对子节点进行diff
操作,调用updateChildren
,这个updateChildren
是diff算法
的核心,后面我们会重点说。
4.新节点不是文本节点,如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
5.新节点不是文本节点,当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
6.新节点不是文本节点,并且新老节点都无子节点的时候,只需要将老节点文本清空。
7.新节点是文本节点,并且新老节点文本不一样,则进行文本的替换。
updateChildren
是diff
算法的核心,下面我们来重点分析。
这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。所以Diff算法是:深度优先算法
。 时间复杂度:O(n)
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 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] // 老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode,同时 oldStartIdx 与 newStartIdx 向后移动一位。 } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode 操作。并将 oldEndVnode 与 newEndVnode 向前移动一位。 } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode 与 newEndVnode 符合 sameVnode 的时候, // 将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。 // 然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // oldEndVnode 与 newStartVnode 符合 sameVnode 时, // 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。 // oldEndIdx 向前移动一位,newStartIdx 向后移动一位。 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // createKeyToOldIdx 的作用是产生 key 与 index 索引对应的一个 map 表 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。 if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] // 如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined // 同时将 newStartVnode.elm 插入到 oldStartVnode.elm 的前面 if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } // 当 while 循环结束以后,如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的, // 需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) // 如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多, // 将这些无用的老节点通过 removeVnodes 批量删除即可。 } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
vue2
的diff
算法采用的是双端比较
,所谓双端比较
就是新列表和旧列表两个列表的头与尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。
我们首先来看看首尾对比的四种情况。
使用旧列表的头一个节点oldStartNode
与新列表的头一个节点newStartNode
对比
使用旧列表的最后一个节点oldEndNode
与新列表的最后一个节点newEndNode
对比
使用旧列表的头一个节点oldStartNode
与新列表的最后一个节点newEndNode
对比
使用旧列表的最后一个节点oldEndNode
与新列表的头一个节点newStartNode
对比
首先是 oldStartVnode
与 newStartVnode
符合 sameVnode
时,说明老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode
,同时 oldStartIdx
与 newStartIdx
向后移动一位。
其次是 oldEndVnode
与 newEndVnode
符合 sameVnode
,也就是两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode
操作并将 oldEndVnode
与 newEndVnode
向前移动一位。
接下来是两种交叉的情况。
先是 oldStartVnode
与 newEndVnode
符合 sameVnode
的时候,也就是老 VNode 节点的头部与新 VNode 节点的尾部是同一节点的时候,将 oldStartVnode.elm
这个节点直接移动到 oldEndVnode.elm
这个节点的后面即可。然后 oldStartIdx
向后移动一位,newEndIdx
向前移动一位。
同理,oldEndVnode
与 newStartVnode
符合 sameVnode
时,也就是老 VNode 节点的尾部与新 VNode 节点的头部是同一节点的时候,将 oldEndVnode.elm
插入到 oldStartVnode.elm
前面。同样的,oldEndIdx
向前移动一位,newStartIdx
向后移动一位。
最後に、上記の状況がいずれも満たされない場合、この状況にどう対処すればよいでしょうか?
それは検索と比較です。
まず、createKeyToOldIdx# を使用して、
oldVnode の
key と
index インデックスに対応する
map## を生成します。 ## メソッド # サーフェス。 これにより、
に基づいて、oldKeyToIdx
(createKeyToOldx
の戻り値) から同じ key# をすばやく取得できます。 ## ノードのインデックス
idxInOld を指定し、同じノードを検索します。
ここには 3 つの状況があります。
newStartIdx## を使用して新しいノードを作成します。 # 1 つ後ろの位置に移動します。 ノードが見つかり、
が実行されます。 unnamed
(後でノードと同じキーを持つ新しいノードが存在する場合、それが検出され、重複キーを持つように求められます)、newStartVnode.elm を挿入します。
を oldStartVnode.elm
の前に置きます。同様に、newStartIdx
は 1 位置後方に移動します。
の子ノードのうち、newStartIdx
が 1 つ戻ります。
これらのノードを挿入するだけです。 同様に、
newStartIdx > newEndIdx
removeVnodes を使用してバッチで削除できます。
#概要
Diff アルゴリズムは比較アルゴリズム
です。 2 つの 仮想ノード を見つけて、この仮想 DOM のみを更新します。ノードに対応する実ノードは、データが変更されていない他のノードを更新するのではなく、実DOMを
正確に更新することで、
効率とパフォーマンスを向上させます。
精度
は主に、
diffアルゴリズムが最初に
再利用可能なノード
Extension
Vue でキーを使用する必要があるのはなぜですか?また、その役割は何ですか?
を介してdiff 操作がより正確かつ高速になります。
の方が正確です。
key
では、sameNode
関数 a.key === b.key でその場で再利用されないためです。
比較中にインプレース再利用を回避できます。したがって、より正確になります。
の一意性を利用して
map オブジェクトを生成し、対応するノードを取得します。これはトラバーサル メソッドよりも高速です。
リストに表示のみが含まれ、並べ替え、削除、追加が含まれない場合に使用しますindex
を index を
key として使用できなくなります。これは、それぞれの
key が異なるためです。要素はそれ以上一意ではありません。これは一意の
key
diff アルゴリズムには役立ちません。これを書くことは書かないことと同じです。
(学習ビデオ共有: Web フロントエンド開発、基本プログラミング ビデオ)
以上がvue2 の VNode と diff アルゴリズムについての深い理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。