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