Rumah > Artikel > hujung hadapan web > Pemahaman mendalam tentang algoritma VNode dan diff dalam vue2
虚拟dom
dan diff
Algoritma ialah titik sukar dalam vue
proses pembelajaran dan titik pengetahuan yang mesti dikuasai semasa temu duga. Kedua-duanya saling melengkapi dan merupakan teras kepada rangka kerja vue
. Hari ini kita akan meringkaskan algoritma vue2
dan 虚拟dom
dalam diff
. (Belajar perkongsian video: tutorial video vue)
Kami tahu bahawa render function
akan ditukar menjadi VNode
. VNode
sebenarnya adalah pokok berdasarkan objek JavaScript
, menggunakan atribut objek untuk menerangkan nod Malah, ia hanyalah lapisan abstraksi DOM
sebenar. Akhirnya, pokok ini boleh dipetakan ke persekitaran sebenar melalui beberapa siri operasi.
Contohnya, jika ia adalah seperti berikut: template
<template> <span class="demo" v-show="isShow"> This is a span. </span> </template>
dan gantikannya dengan VNode
, ia akan kelihatan seperti ini pada masa hadapan
{ 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, }, ], };
Secara amnya, VNode
Hanya objek JavaScript
. Objek JavaScript
ini mewakili sepenuhnya 真实DOM
.
Penulis berpendapat ada dua sebab
Sebab Virtual DOM
jadi JavaScript
Ia berdasarkan objek dan tidak bergantung pada persekitaran platform sebenar, jadi ia mempunyai keupayaan merentas platform, seperti platform penyemak imbas, Weex, Node, dsb.
Kurangkan operasi DOM
Untuk sebarang perubahan halaman, hanya gunakan VNode
untuk perbandingan operasi Hanya kemas kini pemasangan terakhir diperlukan DOM
, mengelakkan operasi yang kerap , mengurangkan aliran semula pelayar dan lukis semula untuk DOM
meningkatkan prestasi halaman.
yang terlibat dalam kemas kini komponen. diff算法
yang diluluskan oleh 渲染watcher
kepada Watcher
sebenarnya ialah kaedah get
. updateComponent
updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)Jadi komponen akan mencetuskan kaedah ini semula apabila data responsif berubah Seterusnya, mari analisa kaedah
dalam updateComponent
secara terperinci. _update
_update
Walaupun kedua-dua kaedah dipanggil, parameter yang diluluskan adalah sama. _update
__patch__
// 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) } // ... }
__patch__
__patch__
ialah patch
dan ialah oldVnode
, jadi pemaparan awal tidak mempunyai vm.$el
. null
oldVnode
// 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 }Proses anggaran kaedah adalah seperti berikut:
patch
Kami akan fokus pada analisis nanti. pathVnode
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) }nod adalah nod yang sama, syarat berikut perlu dipenuhi serentak
VNode
key
tag
isComment
data
mestilah sama <input>
type
s adalah sama dan VNode
ditakrifkan atau tidak ditakrifkan pada masa yang sama , dan jika teg itu tag、key、isComment
mestilah sama. Pada masa ini, kedua-dua data
ini dikira sebagai input则type
dan anda boleh terus melakukan operasi VNode
. sameVnode
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) } }Proses anggaran kaedah adalah seperti berikut:
patchVnode
1 Jika nod lama dan baru adalah sama, kembalikan terus.
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
向后移动一位。
Akhir sekali, apabila tiada satu pun situasi di atas dipenuhi, bagaimana untuk menangani situasi ini?
Itulah cari dan bandingkan.
mula-mula menjana jadual createKeyToOldIdx
sepadan dengan indeks oldVnode
dan key
index
melalui kaedah map
.
Kemudian berdasarkan newStartVnode.key
, kita boleh dengan cepat mendapatkan indeks oldKeyToIdx
nod dengan createKeyToOldIdx
yang sama daripada key
(nilai pulangan idxInOld
), dan kemudian mencari yang sama nod.
Terdapat tiga situasi di sini
Jika nod yang sama tidak ditemui, buat nod baharu melalui createElm
dan gerakkan newStartIdx
ke belakang satu Bit.
Jika nod ditemui dan ia mematuhi sameVnode
, maka kedua-dua nod adalah patchVnode
dan nod lama pada kedudukan itu diberikan nilai undefined
(jika ada masih Jika nod baharu mempunyai kunci yang sama seperti nod, ia boleh dikesan dan digesa bahawa terdapat kunci pendua), dan newStartVnode.elm
dimasukkan di hadapan oldStartVnode.elm
. Dengan cara yang sama, newStartIdx
bergerak ke belakang satu kedudukan.
Jika ia tidak sepadan dengan sameVnode
, anda hanya boleh mencipta nod baharu dan memasukkannya ke dalam nod anak daripada parentElm
. newStartIdx
Berundur satu kedudukan.
Langkah terakhir adalah mudah, apabila while
gelung tamat Pada masa hadapan, jika oldStartIdx > oldEndIdx
, ini bermakna nod lama telah dibandingkan, tetapi masih terdapat banyak nod baharu perlu dimasukkan ke dalam DOM sebenar. Hanya panggil addVnodes
untuk memasukkan nod ini.
Begitu juga, jika syarat newStartIdx > newEndIdx
dipenuhi, bermakna perbandingan nod baru telah selesai dan masih terdapat banyak nod lama yang tidak berguna ini dipadamkan secara berkelompok melalui removeVnodes
Itu sahaja.
Algoritma diff ialah algoritma perbandingan . Bandingkan dua 旧虚拟DOM和新虚拟DOM
, bandingkan 虚拟节点
yang mana telah berubah, cari ini 虚拟节点
dan hanya kemas kini 真实节点
yang sepadan dengan nod maya ini, tanpa mengemas kini nod lain yang datanya tidak berubah, untuk mencapai kemas kini 精准
DOM sebenar, dan kemudian 提高效率和性能
.
精准
terutamanya ditunjukkan dalam fakta bahawa algoritma diff
mula-mula mencari nod boleh guna semula , dan kemudian mengalihkannya ke kedudukan yang betul . Apabila elemen tidak ditemui, buat nod baharu.
key
ialah satu-satunya tanda Vue
dalam vnode
Melalui key
ini, operasi diff
boleh menjadi lebih tepat dan lebih pantas.
key
tiada penggunaan semula di tempat dan penggunaan semula di tempat boleh dielakkan dalam perbandingan sameNode
fungsi a.key === b.key
. Jadi ia akan menjadi lebih tepat. key
untuk menjana map
objek untuk mendapatkan nod yang sepadan, yang lebih pantas daripada kaedah traversal. Gunakannya apabila senarai kami hanya melibatkan paparan, bukan pengisihan, pemadaman atau penambahanindex
Tidak salah untuk menjadi key
. Kerana index
pada masa ini adalah unik pada setiap elemen.
Tetapi apabila ia melibatkan pengisihan, pemadaman dan penambahan, anda tidak boleh lagi menggunakan index
sebagai key
, kerana setiap elemen key
tidak lagi unik. key
bukan unik dan tidak membantu algoritma diff
Menulisnya sama seperti tidak menulisnya.
(Belajar perkongsian video: pembangunan bahagian hadapan web, Video pengaturcaraan asas)
Atas ialah kandungan terperinci Pemahaman mendalam tentang algoritma VNode dan diff dalam vue2. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!