What is the virtual DOM?
The concept of Virtual DOM is familiar to everyone. Simply put, it is a common JavaScript object containing tag, props, and children properties to describe a DOM node. Each group of properties is a VNode. The entire collection of VNodes is a virtual DOM tree.
An 🌰
<div id="app"> <p class="text">hello world!!! </p> </div>Copy the code
Abstract the HTML template above into a virtual DOM tree:
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'hello world!!!'
]
}
]
}
Copy the code
Why do you need the virtual DOM?
-
Have the advantage of cross-platform
Because virtual DOM is based on JavaScript objects and does not depend on the real platform environment, it has the ability of cross-platform, such as browser platform, Weex, Node, etc., is the basis of SSR, small program, etc.
-
Improved rendering performance.
Because the DOM is a large object, manipulating the DOM directly, even an empty DIV, is expensive and not nearly as fast as abstracting Javascript objects, so moving a lot of DOM manipulation into Javascript, Using the Diff algorithm to figure out which nodes really need to be updated minimizes DOM manipulation and significantly improves performance. The advantage of the virtual DOM is not in a single operation, but in a large number of frequent data updates, the view can be reasonably and efficiently updated.
-
Application of the virtual DOM
Render function: cn.vuejs.org/v2/guide/re…
How to update the real DOM with the virtual DOM? – the diff
Diff (Different), as its name implies, in the process of DOM construction, the DIFF process is the place where the DOM changes are calculated by comparison. The core is that the changes are mapped to the real DOM by the Patch algorithm, so the view creation and update process is as follows 👇
-
Using JavaScript object structure to represent the DOM tree structure; Then use this tree to build a real DOM tree and insert it into the document
-
When the state changes, a new object tree is rebuilt. The new tree is then compared with the old tree (diff process), and the differences between the two trees are recorded
-
Apply the differences recorded in 2 to the actual DOM tree (patch) built in Step 1, and the view is updated
Vue diff implementation
Patch algorithm core
function patch (oldVnode, vnode) { // some code if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, Vnode)} else {const oEl = oldvNode. el // Let parentEle = api.parentNode(oEl) // Parent element CreateEle (vnode) // Create new element if (parentEle! == null) {api.insertbefore (parentEle, vnode.el, api.nextsibling (oEl)) OldVnode = null}} // some code return vnode} function sameVnode (a, B) {return (A.cookie === B.cookie && // Key value A.cookie === B.cookie && // Label name A.comment === B.cookie && // Whether it is a comment node // Onclick, style isDef(a.data) === isDef(b.datA) &&sameinputType (a, b) Type must be the same)}Copy the code
The patch function receives two parameters, oldVnode and Vnode, respectively representing the new node and the previous old node, to determine whether the two nodes are worthy of comparison. If the comparison is worthy, patchVnode will be executed; if the comparison is not worthy, oldVnode will be replaced with Vnode
PatchVnode process
patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el let i, oldCh = oldVnode.children, ch = vnode.children if (oldVnode === vnode) return if (oldVnode.text ! == null && vnode.text ! == null && oldVnode.text ! == vnode.text) { api.setTextContent(el, vnode.text) }else { updateEle(el, vnode, oldVnode) if (oldCh && ch && oldCh ! == ch) { updateChildren(el, oldCh, ch) }else if (ch){ createEle(vnode) //create el's children dom }else if (oldCh){ api.removeChildren(el) } } }Copy the code
-
Find the corresponding real DOM, called EL
-
Check whether Vnode and oldVnode refer to the same object. If so, return
-
If they both have text nodes and are not equal, set the text node for Vnode to the text node for EL.
-
If oldVnode has children and Vnode does not, the children of EL are removed
-
If oldVnode does not have children and Vnode does, add Vnode’s children to el after they are materialized
-
If both have child nodes, the updateChildren function is executed to compare the child nodes
updateChildren
This part can be said to be the most changed part of the DIff algorithm, because in the previous part, the direction of the comparison of each library is basically the same, and as for the comparison of child nodes, each warehouse has to constantly improve on the former basis, here Vue source updateChildren as an example.
updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0, 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 let idxInOld let elmToMove let before while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == Null) {// For vnode.key comparison, OldStartVnode = oldCh[++oldStartIdx]}else if (oldEndVnode == null) {oldEndVnode = oldCh[--oldEndIdx] }else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx] }else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode) api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.el, Oldstartvnode. el) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx]}else {// Compare keys if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, } idxInOld = oldKeyToIdx[newStartVNode. key] if (! idxInOld) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] if (elmToMove.sel ! == newStartVnode.sel) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) }else { patchVnode(elmToMove, newStartVnode) oldCh[idxInOld] = null api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el) } newStartVnode = newCh[++newStartIdx] } } } if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx) }else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }Copy the code
The illustration updateChildren
In the example above, the list of nodes in order 1 through 10 is updated, and the list of nodes in order out of order is updated. List the following types of node changes:
-
Nodes with the same head and tail, such as 1 and 10
-
Nodes with the same head and tail: e.g. 2, 9 (after processing nodes with the same head and tail)
-
New node: 11
-
Node to be deleted: 8
-
Other nodes: 3, 4, 5, 6, and 7
In the example above, oldStart+oldEnd and newStart+newEnd are set, corresponding to the start and end of oldVdom and newVdom respectively. Vue continues to process the VNode and move the pointer until any of the pairs of starts and ends meet. The processed node Vue marks it as processed in both oldVdom and newVdom. Vue improves diFF performance by:
1. Handle special scenarios first
-
The position of nodes of the same type in the head and the same type in the tail does not change before and after the update, so there is no need to move their corresponding DOM
-
This type of node is very clear, you don’t need to bother to find, just move the DOM
After processing these scenarios, on the one hand, some DOM that do not need to be moved can be quickly processed; on the other hand, there are fewer nodes to be processed, which reduces the processing scope of subsequent operations and improves performance
2. Reuse
Reuse means that the Vue will reuse the DOM as much as possible, with as little DOM movement as possible. Vue in judging whether the Pointers point to the same node before and after the update, actually does not require them to refer to the same real DOM node, in fact it only to determine whether, pointing to the same node (such as 2 different div, they are not the same in the DOM, but they belong to the same node), if it is a similar nodes, then the Vue will directly reuse the DOM, The advantage is that you don’t need to move the DOM
Reuse should be the difference between setting a key and not setting a key:
Without a key, newCh and oldCh will only compare each other at both ends. After a key is set, according to the implementation of the sameVnode method, it can be known that the key will also be the basis for “whether it is the same node”.
Parse updateChildren procedure (with key set)
1. Handle nodes of the same type for headers. That is, oldStart and newStart point to nodes of the same class, node 1 in the figure below
In this case, the changes to node 1 are updated to the DOM and then marked by oldStart and newStart by 1 bit, without moving the DOM (updating the DOM may be necessary, such as property changes, text changes, etc.).
2. Handle nodes of the same type at the tail. That is, oldEnd and newEnd point to the same type of node, node 10 in the figure below
Similar to case (1), in this case, the changes to node 10 are updated to the DOM, and oldEnd and newEnd are then marked up by 1 bit, again without moving the DOM
3. Handle nodes of the same type for head/tail. That is, oldStart and newEnd, and oldEnd and newStart point to similar nodes, such as nodes 2 and 9 in the figure below
Let’s look at node 2, which is actually moving backwards. Where is it? Move to the node to which oldEnd points (that is, node 9), mark the node after the move, move oldStart back one bit, and move newEnd forward one bit
Similarly, node 9 is then processed in a similar way, resulting in the following
4. Process the newly added nodes
NewStart is at the location of node 11. Node 11 is not found in oldVdom, indicating that it is new
So create a new node, insert it into the DOM tree, insert it where? Insert it in front of the node to which oldStart points (that is, node 3), and then mark newStart one bit later as processed (note that there is no node 11 in oldVdom, so its pointer does not need to be moved during marking)
5. Process updated nodes
After step (4), newStart is at node 7. It can be found in oldVdom and is not in the pointer position (look for nodes between oldStart and oldEnd in oldVdom), indicating that its position has moved
So I need to move it in the DOM tree, where? Move in front of the node pointed to by oldStart (node 3) and mark the node as processed at the same time. This is a little different from the previous cases where newVdom is at the pointer and newStart can be moved to mark the node, whereas oldVdom is not at the pointer. So the notation is set to undefined
6. Process nodes 3, 4, 5, and 6
After the processing of step (5), we see a gratitening scene, newStart and oldStart point to the same node again (that is, they both point to node 3). It is very simple, just move the pointer as shown in step (1), which is very efficient. 3, 4, 5 and 6 are processed in this way
7. Process the node to be deleted
After the first six steps (which are actually in a loop), the friends see newStart crossing over newEnd, and they meet! At this time, oldStart and oldEnd have not yet met, indicating that the nodes between the two Pointers (including the nodes they point to, namely nodes 7 and 8 in the figure above) are the nodes deleted in this update.
OK, so let’s delete them in the DOM tree, and then we go back to the point where we marked node 7, why is that necessary? The purpose of the tag is to tell Vue that it has processed the node that needs to appear in the new DOM and not to delete it, so in this case just delete node 8.
In the application, the start and end points of oldVdom may meet, but the start and end points of newVdom do not meet. In this case, the unprocessed nodes in newVdom need to be processed. Such nodes are added in the update and need to be inserted into the DOM tree.
At this point, the whole diff process is over. The whole process is to gradually find the differences in the VDOM before and after the update, and then reflect the differences into the DOM tree
other
Why not use index as key?
Suppose we have a list like this:
<body>
<div id="app">
<ul>
<li v-for="(value, index) in arr" :key="index">
<test />
</li>
</ul>
<button @click="handleDelete">delete</button>
</div>
</body>
<script>
new Vue({
name: "App",
el: '#app',
data() {
return {
arr: [1, 2, 3]
};
},
methods: {
handleDelete() {
this.arr.splice(0, 1);
}
},
components: {
test: { template: "<li>{{Math.random()}}</li>" }
}
})
</script>
Copy the code
So the initial Vnode list is:
[{tag: "li", key: 0,}}, {tag: "li", key: 0,}, {tag: "Li ", key: 2, // the subcomponent is the third subcomponent, suppose the subcomponent text is 3}];Copy the code
If you look at sameNode, you can only use the key, the tag, the presence of data (regardless of the internal value), the annotation node, and the same input type.
After we perform the delete operation:
[{tag: "li", key: 0, //}}}, {tag: "li", key: 1, //}];Copy the code
Error: index is used for key
-
The original first node text: 1 is reused directly.
-
The original second node text: 2 is reused directly.
-
If the new node is missing, drop the third node (text: 3).
So far, we should have deleted the text: 1 node, and then reused the text: 2 and text: 3 nodes, which became the error of deleting the text: 3 node.