Home > Article > Web Front-end > Introduction to the principle of virtual dom comparison in Vue (example explanation)
This article brings you an introduction to the virtual dom comparison principle in Vue (explanation with examples). It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
Let’s first talk about why there is a virtual dom comparison stage. We know that Vue is a data-driven view (changes in data will cause changes in the view), but you find that when a certain data changes, the view is partial How to accurately find the view corresponding to the data and update it by refreshing instead of re-rendering the whole thing? Then you need to get the DOM structure before and after the data change, find the differences and update it!
Virtual dom is essentially a simple object extracted from the real dom. Just like a simple p contains more than 200 attributes, but what is really needed may only be tagName
, so direct operations on the real dom will greatly affect performance!
The simplified virtual node (vnode) roughly contains the following attributes:
{ tag: 'p', // 标签名 data: {}, // 属性数据,包括class、style、event、props、attrs等 children: [], // 子节点数组,也是vnode结构 text: undefined, // 文本 elm: undefined, // 真实dom key: undefined // 节点标识 }
The comparison of virtual dom is to find out the new node (vnode ) and the old node (oldVnode), and then patch the difference. The general process is as follows
#The whole process is relatively simple. If the old and new nodes are not similar, create DOM directly based on the new node; if they are similar, first compare the data, including class and style. , event, props, attrs, etc., if there is any difference, call the corresponding update function, and then compare the child nodes. The comparison of child nodes uses diff algorithm, which should be the focus and difficulty of this article. Bar.
It is worth noting that during the Children Compare
process, if similar childVnode
is found, they will recursively enter a new window. patching process.
This time the source code analysis is written more concisely. If I write too much, I find myself unwilling to read it (┬_┬)
Let’s look at the patch()
function first:
function patch (oldVnode, vnode) { var elm, parent; if (sameVnode(oldVnode, vnode)) { // 相似就去打补丁(增删改) patchVnode(oldVnode, vnode); } else { // 不相似就整个覆盖 elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } return vnode.elm; }
patch()
The function receives two parameters, the old and new vnode. There is a big difference between the two parameters passed in. Difference: oldVnode's elm
points to the real dom, while vnode's elm
is undefined... But after the patch()
method, vnode's elm
will also point to this (updated) real dom.
The sameVnode()
method to determine whether the old and new vnodes are similar is very simple, which is to compare whether tag and key are consistent.
function sameVnode (a, b) { return a.key === b.key && a.tag === b.tag; }
The solution to the inconsistency between the old and new vnodes is very simple, that is, create a real dom based on the vnode and replace the elm
insertion in the oldVnode DOM document.
For the consistent processing of old and new vnodes is the patching we often mentioned before. What exactly is patching? Just look at the patchVnode()
method:
function patchVnode (oldVnode, vnode) { // 新节点引用旧节点的dom let elm = vnode.elm = oldVnode.elm; const oldCh = oldVnode.children; const ch = vnode.children; // 调用update钩子 if (vnode.data) { updateAttrs(oldVnode, vnode); updateClass(oldVnode, vnode); updateEventListeners(oldVnode, vnode); updateProps(oldVnode, vnode); updateStyle(oldVnode, vnode); } // 判断是否为文本节点 if (vnode.text == undefined) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.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)) { api.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text) } }
Patching is actually calling various updateXXX()
functions to update various attributes of the real dom. Each update function is similar, take updateAttrs()
as an example:
function updateAttrs (oldVnode, vnode) { let key, cur, old const elm = vnode.elm const oldAttrs = oldVnode.data.attrs || {} const attrs = vnode.data.attrs || {} // 更新/添加属性 for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { if (booleanAttrsDict[key] && cur == null) { elm.removeAttribute(key) } else { elm.setAttribute(key, cur) } } } // 删除新节点不存在的属性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key) } } }
The general idea of the update function of the attribute (Attribute
) is:
Traverse the vnode attribute, if it is different from oldVnode, call setAttribute()
to modify;
Traverse the oldVnode attribute, if it is not the same Call removeAttribute()
to delete the vnode attribute.
You will find that there is a judgment of booleanAttrsDict[key]
, which is used to judge whether it is in the Boolean type attribute dictionary.
['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ...]eg :
<video autoplay></video>
, if you want to turn off autoplay, you need to remove this attribute.
After all the data is compared, it’s time to compare the child nodes. First determine whether the current vnode is a text node. If it is a text node, there is no need to consider the comparison of child nodes; if it is an element node, it needs to be considered in three situations:
Both old and new nodes have children. , then enter the comparison of child nodes (diff algorithm);
The new node has children, but the old node does not, then create the dom node in a loop;
The new node does not have children, but the old node does, then delete the dom node in a loop.
The latter two situations are relatively simple. We directly analyze the first situation, comparison of child nodes.
There are many codes in this part of sub-node comparison. Let’s talk about the principle first and then post the code. First look at a picture comparing child nodes:
oldCh
and newCh
in the figure represent the old and new child node arrays respectively. They have their own head and tail pointers oldStartIdx
, oldEndIdx
, newStartIdx
, newEndIdx
, vnode is stored in the array. For easy understanding, it is replaced by a, b, c, d, etc., which represent different types of tags (p, span, p) vnode object.
The comparison of child nodes is essentially a loop comparison of head and tail nodes. The sign of the end of the loop is: the old child node array or the new child node array has been traversed (i.e. oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
). Take a rough look at the cycle process:
The first step compare head to head. If they are similar, the old and new head pointers move backward (i.e. oldStartIdx
&& newStartIdx
), the real DOM remains unchanged, and the next cycle is entered; if they are not similar, enter the second step.
The second step Compare tail to tail. If they are similar, the old end and new end pointers are moved forward (i.e. oldEndIdx--
&& newEndIdx--
), the real DOM remains unchanged, and the next cycle is entered; if they are not similar, the third step is entered. .
The third step Compare head to tail. If they are similar, the old head pointer moves backward and the new tail pointer moves forward (i.e. oldStartIdx
&& newEndIdx--
). The head in the unconfirmed dom sequence is moved to the end and the next cycle is entered. ; Not similar, go to step four.
Step 4 Compare tail and head. If they are similar, the old tail pointer is moved forward and the new head pointer is moved backward (i.e. oldEndIdx--
&& newStartIdx
). The tail in the unconfirmed dom sequence is moved to the head and the next cycle is entered; Not similar, go to step five.
The fifth step is that if the node has a key and the sameVnode is found in the old child node array (both tag and key are consistent), then move its dom to the head of the current real dom sequence. The new head pointer moves backward (i.e. newStartIdx
); otherwise, the dom corresponding to the vnode (vnode[newStartIdx].elm
) is inserted into the head of the current real dom sequence, and the new head pointer moves backward (i.e. newStartIdx
).
Let’s take a look at the situation without the key first, and put an animation to see it more clearly!
# I believe that after reading the picture, you will have a better understanding of the essence of the diff algorithm. The whole process is relatively simple. In the picture above, a total of 6 cycles have been entered, involving each situation. Let’s describe them one by one:
The first time they are all similar (both are a
), The dom does not change, and both the old and new head pointers move backward. a
After the node is confirmed, the real dom sequence is: a,b,c,d,e,f
, the unconfirmed dom sequence is: b,c,d,e,f
;
The second time the tail and tail are similar (both are f
), the dom does not change, and the old and new tail pointers are moved forward. f
After the node is confirmed, the real dom sequence is: a,b,c,d,e,f
, the unconfirmed dom sequence is: b,c,d,e
;
The third time is that the head and tail are similar (both are b
). The head in the current remaining real dom sequence is moved to the end, the old head pointer is moved backward, and the new head pointer is moved backward. The tail pointer moves forward. b
After the node is confirmed, the real dom sequence is: a,c,d,e,b,f
, the unconfirmed dom sequence is: c,d,e
;
The fourth time is that the tail and head are similar (both are e
). The tail in the current remaining real dom sequence is moved to the head, the old tail pointer is moved forward, and the new head pointer is moved behind shift. e
After the node is confirmed, the real dom sequence is: a,e,c,d,b,f
, the unconfirmed dom sequence is: c,d
;
The fifth time is not similar and is directly inserted into the head of the unconfirmed dom sequence. g
After the node is inserted, the real dom sequence is: a,e,g,c,d,b,f
, the unconfirmed dom sequence is: c,d
;
The sixth time is not similar and is directly inserted into the head of the unconfirmed dom sequence. h
After the node is inserted, the real dom sequence is: a,e,g,h,c,d,b,f
, and the unconfirmed dom sequence is: c,d
;
But after ending the loop, there are two situations to consider:
The new byte point array (newCh) has been traversed (newStartIdx > newEndIdx
). Then you need to delete all the redundant old dom (oldStartIdx -> oldEndIdx
). In the above example, it is c,d
;
new The byte point array (oldCh) has been traversed (oldStartIdx > oldEndIdx
). Then you need to add all the extra new dom (newStartIdx -> newEndIdx
).
上面说了这么多都是没有key的情况,说添加了:key
可以优化v-for
的性能,到底是怎么回事呢?因为v-for
大部分情况下生成的都是相同tag
的标签,如果没有key标识,那么相当于每次头头比较都能成功。你想想如果你往v-for
绑定的数组头部push数据,那么整个dom将全部刷新一遍(如果数组每项内容都不一样),那加了key
会有什么帮助呢?这边引用一张图:
有key
的情况,其实就是多了一步匹配查找的过程。也就是上面循环流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,减少dom的操作!
有兴趣的可以看看代码:
function updateChildren (parentElm, oldCh, newCh) { 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, elmToMove, before while (oldStartIdx oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
The above is the detailed content of Introduction to the principle of virtual dom comparison in Vue (example explanation). For more information, please follow other related articles on the PHP Chinese website!