This article is based on SimpRead Transcoding, original address https://juejin.im/post/5affd01551882542c83301da
Preface
The goal is to write a very detailed dry product about diff, so this article is a bit long. You will also use a lot of pictures and code examples to get it together.
Let's start with a few points.
1. How does vue update nodes when data changes?
To know the overhead of rendering real DOM is very large. For example, sometimes we modify some data. If directly rendering to real DOM causes redrawing and rearrangement of the whole DOM tree, is it possible that we only update the small piece of DOM we modified instead of the whole dom? diff algorithm can help us.
We first generate a virtual DOM based on the real DOM. When the data of a node in the virtual DOM changes, a new Vnode will be generated. Then Vnode and old Vnode will be compared. We find that there are some differences between them. We directly modify the real DOM, and then make the value of old Vnode Vnode Vnode Vnode Vnode Vnode.
The diff process is to call a function called patch, compare the old and new nodes, and patch the real DOM while comparing.
2. The difference between virtual DOM and real DOM?
virtual DOM is to extract the real DOM data and simulate the tree structure in the form of objects. For example, DOM is like this:
<div> <p>123</p> </div>
The corresponding virtual DOM (pseudocode):
var Vnode = { tag: 'div', children: [{ tag: 'p', text: '123' }] };
(Warm Tip: VNode and oldVNode are both objects, remember)
3. How does diff compare?
When diff algorithm is used to compare new and old nodes, the comparison will only be carried out at the same level, not across the level.
<div> <p>123</p> </div> <div> <span>456</span> </div>
The above code compares two divs on the same layer and p and span on the second layer, but divs and spans are not compared. A very vivid picture seen elsewhere:
diff flow chart
When the data changes, the set method will call Dep.notify to notify all subscribers of Watcher, and the subscriber will call patch to patch the real DOM and update the corresponding view.
make a concrete analysis
patch
Let's see how patch patches are patched (the code retains only the core part)
function patch(oldVnode, vnode) { // some code if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oEl = oldVnode.el // The current oldVnode corresponds to the real element node let parentEle = api.parentNode(oEl) // Parent element createEle(vnode) // Generate new elements based on Vnode if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // Add a new element to the parent element api.removeChild(parentEle, oldVnode.el) // Remove old element nodes oldVnode = null } } // some code return vnode }
The patch function receives two parameters, oldVnode and Vnode, representing the new node and the old node, respectively.
- To judge whether the two nodes are worth comparing, the patchVnode is executed.
function sameVnode(a, b) { return ( a.key === b.key && // key value a.tag === b.tag && // Label name a.isComment === b.isComment && // Is it a comment node? // Whether data is defined, data contains some specific information, such as onclick, style isDef(a.data) === isDef(b.data) && sameInputType(a, b) // When the label is < input >, the type must be the same ) }
- If not worth comparing, replace old Vnode with Vnode
If both nodes are the same, then go deep into their child nodes. If the two nodes are different, it means that the Vnode has been completely changed, and the old Vnode can be replaced directly.
Although these two nodes are different, what about their children? Don't forget, diff compares layer by layer. If the first layer is different, then it won't go deep into the second layer. (I wonder if this is a disadvantage? The same child node cannot be reused...
patchVnode
When we determine that two nodes are worth comparing, we specify the patchVnode method for the two nodes. So what does this approach do?
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) } } }
This function does the following:
- Finding the corresponding true dom, called el
- Determine whether Vnode and oldVnode point to the same object, and if so, return directly
- If they all have text nodes and are not equal, set el's text nodes to Vnode's text nodes.
- If oldVnode has child nodes and Vnode does not, delete the child nodes of el
- If oldVnode does not have child nodes and Vnode does, add the child nodes of Vnode to el after they are authenticated
- If both have child nodes, it is important to perform the updateChildren function to compare child nodes.
Several other points are well understood. Let's talk about updateChildren in detail.
updateChildren
The amount of code is very large, and it is not convenient to explain one line at a time, so the following is described with some example diagrams.
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, oldVnode = 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 { // A comparison of key s if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // Generate index table 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) } }
Let's start with what this function does.
- Extract the child nodes of Vnode Vch and oldVnode oldCh
- Old Ch and vCh have two head and tail variables, StartIdx and EndIdx, and their two variables are compared with each other. There are four ways of comparison. If none of the four comparisons matches, if key is set, the comparison will be made with key. In the process of comparison, variables will lean to the middle. Once StartIdx > EndIdx indicates that at least one of the oldCh and vCh has been traversed, the comparison will be ended.
Graphical updateChildren
Finally came to this part, the above summary believe that many people also see the face of confusion, let's talk about it. (I drew them all by myself. I would like to recommend some useful drawing tools...)
The pink part is oldCh and vCh
We take them out and point them to their head child and tail child with s and e pointers, respectively.
There are four ways to compare old S, old E, S and E. When two of them can match, the corresponding nodes in the real dom will move to the corresponding position of the Vnode. This sentence is somewhat circumvented. Let's make an analogy.
- If oldS and E match, the first node in the real dom moves to the end.
- If oldE and S match, the last node in the real dom moves to the front, and the two pointers on the match move to the middle.
- If none of the four matches is successful, there are two cases
- If there are keys in both old and new sub-nodes, a hash table will be generated according to Old Child's key. The key of S will be matched with the hash table. If the matching is successful, the S and matching node will be sameNode. If so, the successful node will be moved to the front of the real dom. Otherwise, the corresponding node generated by S will be inserted into do. The corresponding oldS position in m, oldS and S pointer move to the middle.
- If there is no key, then the new node generated by S is directly inserted into the real DOM (ps: This can explain why the key is needed when v-for is set. If there is no key, then only four matches will be made, even if there are reusable nodes in the pointer can not be reused).
Add a graph (assuming that all nodes in the graph below have keys and keys are their own values)
- First step
oldS = a, oldE = d; S = a, E = b;
When oldS matches S, the A node in the dom is placed in the first one, which is already the first one. At this time, the location of the dom is: a b d.
- Step 2
oldS = b, oldE = d; S = c, E = b; //Copy code
When oldS and E match, the original b-node is moved to the end, because E is the last node, and their positions should be the same. That is what I said above: when two of them can match, the corresponding nodes in the real dom will move to the corresponding position of Vnode, and then the position of dom is a db:
- Step 3
oldS = d, oldE = d; S = c, E = d;
Old E matches E, and the position of dom is a d b when the position is unchanged.
- Step 4
oldS++; oldE--; oldS > oldE;
At the end of the traversal, oldCh completes the traversal first. The remaining vCh nodes are inserted into the real dom according to their own index, and the dom location is: a c d b
A simulation is completed.
There are two conditions for the end of this matching process:
- Old S > OlE means that the old Ch is traversed first, then the extra vCh is added to the dom according to index (as shown above).
- S > E means that vCh traverses first and then deletes the redundant nodes with interval [oldS, oldE] in the real dom
Here's another example. You can try to simulate it yourself as above.
When these nodes sameVnode succeeds, the patchVnode will be executed immediately. Take a look at the code above.
if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) }
This is done layer by layer until all the child nodes in old Vnode and Vnode are compared. All the dom patches have been patched, too. Would it be much easier to go back and see the updateChildren code now?
summary
Above is the whole process of diff algorithm. Put on a summary chart which was sent at the beginning of the article. You can try to look at this chart and recall the process of diff.
Welcome to the comments section for more exchanges.