preface
Hello, everyone, in the daily interview, Diff algorithm is a barrier that can't be bypassed. In the most popular words, speaking about the most difficult knowledge points has always been the purpose of my article. Today I'll explain Diff algorithm in a popular way? Lets Go
What is virtual DOM
Before talking about Diff algorithm, let me tell you what virtual DOM is. This is conducive to a deeper understanding of the Diff algorithm.
Virtual DOM is an object. What kind of object is it? An object used to represent the real DOM, remember this sentence. Let me give you an example. Look at the following real DOM:
<ul id="list"> <li>ha-ha</li> <li>ha-ha</li> <li>hey</li> </ul>
The corresponding virtual DOM is:
let oldVDOM = { // Old virtual DOM tagName: 'ul', // Tag name props: { // Label properties id: 'list' }, children: [ // Signature sub node { tagName: 'li', props: { class: 'item' }, children: ['ha-ha'] }, { tagName: 'li', props: { class: 'item' }, children: ['ha-ha'] }, { tagName: 'li', props: { class: 'item' }, children: ['hey'] }, ] }
At this time, I modify one li tag Text for:
<ul id="list"> <li>ha-ha</li> <li>ha-ha</li> <li>Lin Sanxin hahaha</li> // modify </ul>
Generated at this time New virtual DOM Is:
let newVDOM = { // New virtual DOM tagName: 'ul', // Tag name props: { // Label properties id: 'list' }, children: [ // Signature sub node { tagName: 'li', props: { class: 'item' }, children: ['ha-ha'] }, { tagName: 'li', props: { class: 'item' }, children: ['ha-ha'] }, { tagName: 'li', props: { class: 'item' }, children: ['Lin Sanxin hahaha'] }, ] }
This is what we usually call the old and new virtual DOMS. At this time New virtual DOM Is the latest status of the data, so let's take it directly New virtual DOM To render into Real DOM Is it really more efficient than directly operating a real DOM? It certainly won't. look at the figure below:
From the above figure, we can see that the second method must be faster, because there is one in the middle of the first method Virtual DOM Steps, so Virtual DOM is faster than real dom This sentence is actually wrong, or not rigorous. What is the correct statement? Virtual DOM algorithm operates real DOM, and its performance is higher than that of directly operating real dom. Virtual Dom and virtual DOM algorithm are two concepts. Virtual DOM ALGORITHM = Virtual DOM + Diff algorithm
What is Diff algorithm
We said above Virtual DOM, I also know that only Virtual DOM + Diff algorithm can really improve performance. That's all Virtual DOM, let's talk about it again Diff algorithm Let's go back to the above example (the compressed image is a little small. You can open it and see it clearly):
In the figure above, in fact, only one li tag modifies the text, and the others remain unchanged. Therefore, it is not necessary to update all nodes. Just update this li tag. Diff algorithm is the algorithm to find this li tag.
Summary: Diff algorithm is a comparison algorithm. Compare the old virtual Dom and the new virtual DOM, compare which virtual node has changed, find out the virtual node, and only update the real node corresponding to the virtual node without updating the node whose other data has not changed, so as to accurately update the real Dom and improve efficiency.
Loss calculation using virtual DOM algorithm: total loss = Virtual DOM addition, deletion and modification + (related to the efficiency of Diff algorithm) real DOM difference addition, deletion and modification + (fewer nodes) typesetting and redrawing
Loss calculation of direct operation of real DOM: total loss = complete addition, deletion and modification of real DOM + (there may be more nodes) typesetting and redrawing
Principle of Diff algorithm
Diff same layer comparison
When comparing new and old virtual DOM S, Diff algorithm will only be compared at the same level, not cross level. So Diff algorithm is: depth first algorithm. Time complexity: O(n)
Diff comparison process
When the data changes, the setter will be triggered and all subscribers will be notified of the Watcher through Dep.notify. The subscribers will call the patch method to patch the real DOM and update the corresponding view. For those who don't know much about this step, you can take a look at what I wrote before Vue source code series
newVnode and oldVnode: old and new virtual nodes in the same layer
patch method
This method is used to compare whether the current virtual nodes on the same layer are labels of the same type (the standards of the same type will be described below):
- Yes: continue to execute the patchVnode method for deep comparison
- No: there is no need to compare. Replace the whole node with New virtual node
Let's take a look at the core principle code of patch
function patch(oldVnode, newVnode) { // Compare whether it is a type of node if (sameVnode(oldVnode, newVnode)) { // Yes: continue to conduct in-depth comparison patchVnode(oldVnode, newVnode) } else { // no const oldEl = oldVnode.el // Real DOM node of old virtual node const parentEle = api.parentNode(oldEl) // Get parent node createEle(newVnode) // Create a real DOM node corresponding to the new virtual node if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // Add new element to parent element api.removeChild(parentEle, oldVnode.el) // Remove previous old element nodes // Set null to free memory oldVnode = null } } return newVnode }
sameVnode method
The key step of patch is The sameVnode method determines whether it is a node of the same type. The problem arises. How can it be regarded as a node of the same type? this type What are the criteria for?
Let's have a look sameVnode The core principle code of the method is clear at a glance
function sameVnode(oldVnode, newVnode) { return ( oldVnode.key === newVnode.key && // Is the key value the same oldVnode.tagName === newVnode.tagName && // Is the tag name the same oldVnode.isComment === newVnode.isComment && // Are all annotation nodes isDef(oldVnode.data) === isDef(newVnode.data) && // Are data defined sameInputType(oldVnode, newVnode) // When the tag is input, must the type be the same ) }
patchVnode method
This function does the following:
- Find the corresponding Real DOM, called el
- judge newVnode and oldVnode Whether to point to the same object. If so, directly return
- If they all have text nodes and are not equal, then el The text node of is set to newVnode Text node for.
- If oldVnode There are child nodes and newVnode If not, delete el Child nodes of
- If oldVnode Without child nodes newVnode If yes, it will newVnode The child nodes of are added to el after they are materialized
- If both have child nodes, execute updateChildren Function to compare child nodes, which is very important
-
function patchVnode(oldVnode, newVnode) { const el = newVnode.el = oldVnode.el // Get real DOM object // Gets the child node array of the old and new virtual nodes const oldCh = oldVnode.children, newCh = newVnode.children // Terminate if the old and new virtual nodes are the same object if (oldVnode === newVnode) return // If the old and new virtual nodes are text nodes and the text is different if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) { // The text in the real DOM is directly updated to the text of the new virtual node api.setTextContent(el, newVnode.text) } else { // otherwise if (oldCh && newCh && oldCh !== newCh) { // Both old and new virtual nodes have child nodes, and the child nodes are different // Compare child nodes and update updateChildren(el, oldCh, newCh) } else if (newCh) { // The new virtual node has child nodes, but the old virtual node does not // Create a child node of the new virtual node and update it to the real DOM createEle(newVnode) } else if (oldCh) { // The old virtual node has child nodes, but the new virtual node does not //Directly delete the corresponding child nodes in the real DOM api.removeChild(el) } } }
The other points are well understood. Let's talk about them in detail updateChildren
updateChildren method
This is patchVnode The most important method in is to compare the child nodes of the old and new virtual nodes, which occurs in the updateChildren method. Next, let's talk about it in combination with some figures to make you better understand it
What is a comparison method? namely Head and tail pointer method , The new child node set and the old child node set have two pointers at the beginning and end, for example:
<ul> <li>a</li> <li>b</li> <li>c</li> </ul> After modifying data <ul> <li>b</li> <li>c</li> <li>e</li> <li>a</li> </ul>
Then the new and old child node sets and their head and tail pointers are:
Then they will compare with each other. There are five comparison situations:
Next, take the above code as an example to analyze the comparison process
Before analysis, please remember that the final rendering result should be subject to newVDOM, which also explains why subsequent node movements need to be moved to the corresponding position of newVDOM
First step
oldS = a, oldE = c newS = b, newE = a
Comparison results: oldS and newE Equal, you need to move node a to the position corresponding to newE, that is, the end, and oldS + +, newE--
Step 2
oldS = b, oldE = c newS = b, newE = e
Comparison result: oldS and news are equal. You need to Node b Move to newS At the same time, oldS++,newS++
Step 3
oldS = c, oldE = c newS = c, newE = e
Comparison result: oldS, oldE and news are equal, so it is necessary to Node c Move to newS At the same time, oldS++,newS++
Step 4
Olds > olde, then oldCh The traversal is complete first, and newCh I haven't finished traversing. It means There are more newch than oldch, so you need to insert the extra nodes into the corresponding positions on the real DOM
I'll leave you a thinking question here. The example above is Newch is more than oldch. If the opposite is true, yes Oldch ratio If there are many new ch, that is newCh Go through the cycle first, and then oldCh There will be more nodes. As a result, these old nodes will be deleted in the real DOM. You can think for yourself and simulate this process. Like me, drawing and simulation can consolidate the above knowledge.
enclosed updateChildren Core principles of code
function 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) { 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 { // Comparison when using key if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // index table generated with key } 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) } }
Use index as key
usual v-for Why not use index as the key of the loop item when rendering in a loop?
Let's take an example. The initial data is on the left, and then I insert a new data in front of the data to become a list on the right
<ul> <ul> <li key="0">a</li> <li key="0">Lin Sanxin</li> <li key="1">b</li> <li key="1">a</li> <li key="2">c</li> <li key="2">b</li> <li key="3">c</li> </ul> </ul>
Logically speaking, the most ideal result is to insert only one li tag new node and leave the others unchanged to ensure the highest efficiency of DOM operation. But if we use index as the key here, will we really achieve our ideal result? Without much nonsense, practice it:
<ul> <li v-for="(item, index) in list" :key="index">{{ item.title }}</li> </ul> <button @click="add">increase</button> list: [ { title: "a", id: "100" }, { title: "b", id: "101" }, { title: "c", id: "102" }, ] add() { this.list.unshift({ title: "Lin Sanxin", id: "99" }); }
Click the button and we can see that not the result we expected, but all li tags have been updated
Why? Or through the diagram to explain
Logically, a, b, c The three li tags are all before reuse, because they have not changed at all. What has changed is that a new Lin Sanxin has been added in front
But as we said earlier, we are in the process of child node diff algorithm In the process, the Comparing the sameNode of the old head node with the sameNode of the new head node, this step hits the logic, because now the old and new head nodes are used twice of key All 0. Similarly, the nodes with 1 and 2 keys also hit the logic, resulting in the node with the same key patchVnode Update the text, which is already there c node , However, because there was no node with key 4 before, it was regarded as a new node, so it was funny. Using index as the key, the last new node was the existing c node. So the first three are carried out patchVnode Update the text, and the last one is updated newly added , That explains why all li tags have been updated.
How can we solve it? In fact, we just need to use a unique value as the key
<ul> <li v-for="item in list" :key="item.id">{{ item.title }}</li> </ul>
Now let's see the effect
Why do we use id as the key to achieve our ideal effect? Because if we do so, the keys of nodes a, b and c will never change. The keys before and after the update are the same, and because the contents of nodes a, b and c have not changed, it is even changed patchVnode also does not perform complex update operations inside, saving performance. However, Lin Sanxin node is treated as a new node and added to the real DOM because there is no node corresponding to its key before updating.
epilogue
I hope it can help those students who have always wanted to understand virtual DOM and Diff algorithm
If you think this article can help you a little, please give me a praise, ha ha
You are welcome to point out my mistakes and I will correct them in time