Writing an article is not easy, click a like brother focus on Vue source code sharing, the article is divided into vernacular version and source version, vernacular version to help understand the working principle, source version to help understand internal details, let us study together based on Vue version [2.5.17]
If you think the layout is ugly, please click the link below or pull to the following public account can also be
Vue principle Diff – source version of the Diff process
Today is the day to finally explore the focus of Vue’s DOM update, the Diff
Diff isn’t much to talk about, but if it’s going to be detailed, it’s a lot to talk about, and a lot of pictures
This is Diff’s last article, the most important and detailed
So this is a lot of content, so let me give you an overview
1, analysis of Diff source comparison step 2, personal thinking why so comparison 3, write an example, step by step to go through a Diff processCopy the code
The article is very long, also very detailed, if you are interested in this content, also recommend reading the source code while reading, if you do not have a temporary understanding of this content, you can first look at the vernacular version of the source code does not involve Diff – vernacular version
Let’s begin our text
In the previous article Diff – Source edition from New Instance to Start Diff, we explored how Vue goes from new instance to start Diff
You should remember that one of the important functions Diff involves is createPatchFunciton
var patch = createPatchFunction();
Vue.prototype.__patch__ = patch
Copy the code
So let’s look at this function
createPatchFunction
function createPatchFunction() {
return functionPatch (oldVnode, vNode, parentElm, refElm) {// There are no old nodes, and new nodes are generated directlyif(! oldVnode) { createElm(vnode, parentElm, refElm); }else{// It is the same Vnodeif(sameVnode(oldVnode, vnode)) {// Compare existing root node patchVnode(oldVnode, vnode); }else{// Replace existing elements var oldElm = oldvNode.elm; // create a new node createElm(vnode, _parentElm, oldelm.nextsibling); // Destroy the old nodeif(_parentElm) { removeVnodes([oldVnode], 0, 0); }}}return vnode.elm
}
}
Copy the code
And what this function does is
What is the difference between the new node and the old node, and then complete the update
So you see receiving an oldVnode and a VNode
The processing process is divided into
2. The old node is the same as the new node itself (excluding its children). 3Copy the code
Let’s speed up the three processes
1 There is no old node
There are no old nodes, indicating that the page is just starting to initialize, at this point, there is no need to compare
It’s all new, so just call createElm
2 The old node is the same as the new node itself
The sameVnode function is used to determine whether the nodes are the same, as described in the previous article
When the old node is the same as the new node itself, patchVnode is directly called to process the two nodes
PatchVnode is described below
Before talking about patchVnode, what is the function of this function?
What do we need to do when two VNodes are themselves the same?
The tag and key attributes of a Vnode are the same
So, we don’t know if the children are the same, so we definitely need to compare the children
Therefore, one of the functions of patchVnode is to compare child nodes
3 The old node is different from the new node
When two nodes are not the same, it is not difficult to understand, directly create a new node, delete the old node
patchVnode
In the previous function createPatchFunction, there was a function patchVnode
We consider that one of the functions of this function is to compare the children of two VNodes
Is it what we want? We can have a look at the source code first
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) returnvar elm = vnode.elm = oldVnode.elm; var oldCh = oldVnode.children; var ch = vnode.children; / / update the childrenif(! Vnode.text) {// oldCh and ch existif (oldCh && ch) {
if(oldCh ! == ch) updateChildren(elm, oldCh, ch); } // if newCh exists, oldCh must not exist. If newCh exists, oldCh must not existelse if (ch) {
if (oldVnode.text) elm.textContent = ' ';
for(var i = 0; i <= ch.length - 1; ++i) { createElm( ch[i],elm, null ); }}else if (oldCh) {
for(var i = 0; i<= oldCh.length - 1; ++i) { oldCh[i].parentNode.removeChild(el); }}else if (oldVnode.text) {
elm.textContent = ' '; }}else if (oldVnode.text !== vnode.text) {
elm.textContent = vnode.text;
}
}
Copy the code
So let’s analyze this function now
Yes, as we thought, this function does compare and process child nodes
So in general, what this function does is
1, Vnode is a text node, update text (text node does not have child nodes)
2. Vnode has child nodes
A further summary is that this function does two main kinds of judgment processing
1. Vnode is a text node
2. Check whether the Vnode has child nodes
Let’s look at a detailed analysis of these steps
1 Vnode is a text node
A VNode is a text node when it has the text attribute
Let’s start by looking at what a text Vnode looks like
So when a Vnode is a text node, all you need to do is update the text
There are also two treatments
1. When a new vnode. text exists and is different from the old vnode. text
Update the text content of the DOM directly
elm.textContent = vnode.text;
Copy the code
Note: textContent is a property of the real DOM that holds the text of the DOM, so update this property directly
2, the text of the new Vnode is null, directly assign the text DOM to null
elm.textContent = ' ';
Copy the code
2 The Vnode has child nodes
When a Vnode has child nodes, it is not known whether the child nodes of the old and new nodes are the same, so it needs to be compared to complete the update
There are three treatments
1. Both old and new nodes have children, and they are different
2. Only new nodes are available
3. Only old nodes are available
The last two nodes, I’m sure you can figure it out, but let’s just say it
1 Only new nodes exist
There’s only new nodes, there’s no old nodes, there’s no comparison, all nodes are brand new
So just create all the new DOM, which is to create all the new DOM and add it to the parent node
2 Only old nodes exist
There are only old nodes but no new nodes, indicating that the updated page, the old nodes are missing
So all you have to do is delete all the old nodes
So you just delete the DOM
3 Both old and new nodes have children, and they are different
Gee, there’s a new function, update Dren
Notice that this function is very important. It is the core module of Diff and contains the idea of Diff
It may be a little convoluted, but don’t worry, I believe that with my exploration, I can make a little bit more sense
Again, let’s think about the role of Update Dren
Remember the condition, when the new node and the old node both exist, how do you compare them so you know what the difference is?
Oh yes, using traversal, the new child is compared to the old child
If it’s the same, don’t update it, if not, update it
Let’s test our ideas and explore the updateChildren source code
updateChildren
This function is very long, but it is not difficult, just several processes, but at first it may seem a bit confusing
Or you can skip the source code and look at the analysis, or you can look at the analysis and look at the source code
functionupdateChildren(parentElm, oldCh, newCh) { var oldStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newStartIdx = 0; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, vnodeToMove, refElm; // Continuously update OldIndex and OldVnode, newIndex and newVnodewhile (
oldStartIdx <= oldEndIdx &&
newStartIdx <= newEndIdx
) {
if(! oldStartVnode) { oldStartVnode = oldCh[++oldStartIdx]; }else if(! oldEndVnode) { oldEndVnode = oldCh[--oldEndIdx]; } // Compare the old header with the new headerelse if(sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } // The old tail is compared with the new tailelse if(sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } // Compare the old head with the new tailelse if(sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode); // Put oldStartVnode after oldEndVnode, Also find behind oldEndValue node parentElm. InsertBefore (oldStartVnode. Elm, oldEndVnode. Elm. NextSibling); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } // The old tail is compared with the new headelse if(sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode); Parentelm. insertBefore(oldEndvNode. elm, oldStartvNode. elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } // A single new child node looks up a location in the old child node arrayelse{// oldKeyToIdx is a map that converts Vnode keys to indexif(! oldKeyToIdx) { oldKeyToIdx = createKeyToOldIdx( oldCh, oldStartIdx, oldEndIdx ); } // Use newStartVnode to find the same node in OldMap. The default key exists in idxInOld = oldKeyToIdx[newStartvNode. key]. // In the new child, a new node exists, but the old node does not existif(! CreateElm (newStartVnode, parentElm, oldStartVNode.elm); }elseVnodeToMove = oldCh[idxInOld]; vnodeToMove = oldCh[idxInOld];if(sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode); OldCh [idxInOld] = undefined; Parentelm. insertBefore(vnodeTomove.elm, oldStartvNode.elm); } // Only one new node can be created and inserted into the children of parentElmelse{ // same key but different element. treat as new element createElm( newStartVnode, parentElm, oldStartVnode.elm ); // newStartVnode = newCh[++newStartIdx]; // newStartVnode = newCh[++newStartIdx]; }} // Process the remaining nodesif (oldStartIdx > oldEndIdx) {
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
for(; newStartIdx <= newEndIdx; ++newStartIdx) { createElm( newCh[newStartIdx], parentElm, refElm ); }} // The new nodes are compared, and the old nodes may still exist. Delete the remaining old nodeselse if (newStartIdx > newEndIdx) {
for(; oldStartIdx<=oldEndIdx; ++oldStartIdx) { oldCh[oldStartIdx].parentNode.removeChild(el); }}}Copy the code
First of all, what does this function deal with
We’re dealing with the new and old child nodes, and we’re doing a loop over one by one
How do I loop through it?
1. Use while
2. Both the old and new node arrays are configured with the first and last indexes
Two indexes for the new node: newStartIdx and newEndIdx
Two indexes of the old node: oldStartIdx and oldEndIdx
Traversal in the form of two sides enclosing in the middle
When the children of the header are compared, startIdx is incremented by 1
When the children at the end are compared, endIdex decreases by 1
As soon as one of the arrays completes traversal (startIdx<endIdx), the traversal is complete
The source code process is divided into two parts
1. Compare the old and new child nodes
2. After the comparison, process the remaining nodes
Let’s walk through the two processes one by one
1 Compare the old and new child nodes
Note: There are two arrays, a new child Vnode array and an old child Vnode array
During the comparison, no changes are made to the two arrays (e.g. no inserts, no subitems are deleted)
And all the comparison process is directly insert and delete the real page DOM
Let’s be clear, what’s the purpose of the comparison?
Find the same child node between the old and new child nodes, and update the DOM as much as possible by moving instead of creating
It will only be created if it’s really different
Compare the update plan steps
First of all, don’t move the DOM
Second, move the DOM
Finally, consider creating/deleting the DOM
Try not to move if you can. Move if you can’t, build if you can’t
Let’s start with the comparison logic in the source code
The five comparative logics are as follows
1, old head == new head 2, old tail == new tail 3, old head == new tail 4, old tail == new head 5, single searchCopy the code
Let’s analyze the five comparative logics
1 Old head == new head
sameVnode(oldStartVnode, newStartVnode)
Copy the code
When two old and two new heads are the same, what do you do
Following our first step, do not move the DOM to complete the update
But patchVnode
To continue processing the children of the same two nodes, or update the text
Since we’re not considering multi-layer DOM structures, we’re done here if the old and new heads are the same
You can go straight to the next cycle
NewStartIdx ++, oldStartIdx ++Copy the code
2 Old tail == new tail
sameVnode(oldEndVnode, newEndVnode)
Copy the code
The same treatment as the head
End to end, jump straight into the next loop
NewEndIdx ++, oldEndIdx ++Copy the code
3 Old head == new tail
sameVnode(oldStartVnode, newEndVnode)
Copy the code
This step does not comply with not moving the DOM, so you have to move the DOM
How do you move it?
The source code looks like this
parentElm.insertBefore(
oldStartVnode.elm,
oldEndVnode.elm.nextSibling
);
Copy the code
Moves in the position of the new child, with the old head at the end of the new child
So put oldStartVnode’s DOM after oldEndVnode
But because there is no way to put the DOM after anyone, insertBefore is the only option
That is, before the node following oldEndVnode
It looks something like this
Then update both indexes
OldStartIdx++ newEndIdx -Copy the code
4 Old tail == new head
sameVnode(oldEndVnode, newStartVnode)
Copy the code
It also doesn’t apply to not moving the DOM, so you can only move the DOM
How do you move it?
parentElm.insertBefore(
oldEndVnode.elm,
oldStartVnode.elm
);
Copy the code
Place the oldEndVnode DOM directly in front of the current oldStartvNode.elm
It looks something like this
Then update both indexes
NewStartIdx++ oldEndIdx -Copy the code
5 Single traversal search
This is a last resort when all four comparisons fail
Take the children of the new child node and go directly through the old child node array to find the same node
The process looks like this
1. Generate a map table with vnode.key as the key of the old child node array
2. Get a child of the new child node array and check whether its key is in the map above
3. If the DOM does not exist, create a DOM
4. If yes, check whether sameVnode exists
Let’s talk about it in detail
1 Generate a Map table
So what this map does is basically determine what old child nodes are there
Let’s say your old array of child nodes is
[{
tag:"div", key:1
},{
tag:"strong", key:2
},{
tag:"span", key:4
}]
Copy the code
CreateKeyToOldIdx generates a map oldKeyToIdx
{vnodeKey: array Index}
The attribute name is vNode. key, and the attribute value is the location of the vnode in children
This is true (see Diff – source code version of the related helper function)
oldKeyToIdx = {
1:0,
2:1,
4:2
}
Copy the code
2 Check whether the new child node exists in the old child node array
Get Vnode, the child of the new child node, and then get its key
To match the map table, check whether the nodes are the same
oldKeyToIdx[newStartVnode.key]
Copy the code
3 does not exist in the old child node array
Create the DOM directly and insert it in front of oldStartVnode
createElm(newStartVnode, parentElm, oldStartVnode.elm);
Copy the code
4 is stored in the old child node array
Find the old child node and determine whether the new child node and the old child node are samevnodes
If the same, move it directly to the front of oldStartVnode
If not, create it directly before inserting oldStartVnode
We said there are two processes for comparing child nodes
1. Compare the old and new child nodes
2. After the comparison, process the remaining nodes
Comparing the old and new child nodes has been covered above, and now comes another process, comparing the remaining nodes, as detailed below
Process possible remaining nodes
In updateChildren, after comparing the old and new arrays, it is possible that one of the arrays will have a portion of its nodes left unprocessed, so it needs to be treated uniformly
1 The new child node is traversed
newStartIdx > newEndIdx
Copy the code
When the new child node is traversed, the old child node may remain
So we need to batch delete the remaining old nodes!
That is, iterating through the remaining nodes, deleting the DOM one by one
for (; oldStartIdx <= oldEndIdx; ++oldStartIdx) {
oldCh[oldStartIdx]
.parentNode
.removeChild(el);
}
Copy the code
2 The old child node is traversed
oldStartIdx > oldEndIdx
Copy the code
If the old child node is traversed, the new child node may remain
So we have to deal with the rest of the new child nodes
Obviously, the remaining new children do not exist in the old children, so they are all new
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
createElm(
newCh[newStartIdx],
parentElm,
refElm
);
}
Copy the code
But there is a problem with new, that is, where to plug?
So one of the refElm has become a suspect, look at the source code
var newEnd = newCh[newEndIdx + 1]
refElm = newEnd ? newEnd.elm :null;
Copy the code
RefElm gets the node after newEndIdx
The node that is not currently being processed is newEndIdx
So newEndIdx+1, if it exists, must have been processed
If newEndIdx has never been moved and is always the last bit, then there is no newCh[newEndIdx + 1]
Then refElm is empty, and the remaining new nodes are added to the end of the child of the parent node, equivalent to
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
parentElm.appendChild(
newCh[newStartIdx]
);
}
Copy the code
If newEndIdx has been moved, it is added one by one in front of refElm, equivalent to
for (; newStartIdx <= newEndIdx; ++newStartIdx) {
parentElm.insertBefore(
newCh[newStartIdx] ,
refElm
);
}
Copy the code
As shown in figure
Why do you compare
Now that we’ve covered all the Diff, you should get the idea of the Diff
But I forced myself to think about one thing, which is
Why do you compare them like this?
The following is purely personal fantasy ideas, no authority certification, only for reference
All of our comparisons are to find new child nodes that are the same as old child nodes
And the purpose of our comparative treatment is
1, can not move, try not to move
There is no way, but to move
3. If it is impossible, create or delete it
First of all, at the beginning of the comparison, it must be in accordance with our first principle not to move, to find nodes that can not move
And head, tail, tail is more in line with our first principle, so it comes at the beginning, well, that makes sense
Then we move on to our second purpose, which is to follow update Dren
Old head new tail comparison, old tail new head comparison, single lookup comparison
I’m starting to wonder, huh? Head to tail comparison to move I know, but why do we have this comparison?
When I can do all the moves in a single search, right?
I thought for a long time about the relationship between head and tail, and thought it might be to avoid extreme consumption??
How do you say?
For example, when we do a head-to-tail comparison, we use a single lookup all together
If the head and tail nodes are the same, a node has to traverse from the beginning to the end to find the same node
This is just too expensive, so the head-to-tail comparison is added to eliminate the cost of extreme cases
Of course, this is just my opinion for reference, although I did do an example test with that said
Add b div to the child node where two head-to-tail comparisons occur
oldCh = ['header'.'span'.'div'.'b']
newCh = ['sub'.'b'.'div'.'strong']
Copy the code
Use Vue to update, compare the update speed, then update ten times, calculate the average
1. A single search was used to complete the search, which took 0.91ms
2. It took 0.853ms to add the head-to-tail comparison
It is faster
Walk the process
I believe that after such a long article, you have not put all knowledge points together in your mind, may be a little fuzzy to the whole process
That’s ok. Let’s take an example right now and go through the process of updating
For the following nodes, green means unprocessed, gray means processed, light green means being processed, and red means newly inserted, as shown below
Now Vue needs to be updated, there are two sets of old and new child nodes, which need to be compared to determine which nodes need to be updated
1 header comparison, the same node, no need to move, just update the index
Update index newStartIdx++, oldStartIdx++
Start next processing
After a series of judgments, [old head 2] is the same as [new tail 2] and moves directly after oldEndVnode
Update index newEndIdx–, oldStartIdx ++
Start next processing
3 after a series of judgments, [old head 2] is the same as [new tail 2] and moves directly to the front of oldStartVnode
Update index oldEndIdx– newStartIdx++
Start the next round of comparison
4 only one node, go to the last judgment, a single search
If you cannot find the same one, insert it directly in front of oldStartVnode
Update index newStartIdx++
At this point newStartIdx> newEndIdx completes the loop
5 Delete remaining old nodes in batches
OldStartIdx and oldEndIdx both point to the same node, so just delete oldvNode-4
Ok, complete all comparison processes
Yeah, that’s all for Diff. Thank you for watching
The last
In view of my limited ability, there will inevitably be omissions and mistakes, please forgive me, if there is any improper description, welcome to contact me, thanks