In VUE, the patch process is based on the new virtual DOM to transform the old virtual DOM.

Macroscopically speaking, patch process does three things:

  • Create a node
  • Update the node
  • Remove nodes

Next, we take them one by one.

An update.

After the render function returns the virtual DOM, vue performs the update method to update the view. The trunk code is as follows:


 Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
   const vm: Component = this
   const prevEl = vm.$el
   const prevVnode = vm._vnode
   
   const restoreActiveInstance = setActiveInstance(vm)
   vm._vnode = vnode
   
   if(! prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating,false /* removeOnly */)}else {
     vm.$el = vm.__patch__(prevVnode, vnode)
   }
   restoreActiveInstance()
   
   // ...
 }
 

Copy the code

setActiveInstance

 export let activeInstance: any = null

 export function setActiveInstance(vm: Component) {
   const prevActiveInstance = activeInstance
   activeInstance = vm
   return () = > {
     activeInstance = prevActiveInstance
   }
 }
Copy the code

In the previous chapter, we analyzed the componentization practice. The setActiveInstance method sets which component is currently active. Because only one component is instantiated at a time.

The activeInstance variable is the component object that is currently being instantiated. The prevActiveInstance is actually the parent instantiated object. After each child component is instantiated and patched, the restoreActiveInstance method is executed, resetting the current activeInstance to the current parent component, and so on up to the uppermost Vue.

The activeInstance function is used when the component is instantiated. The activeInstance function is used when the component is instantiated. The activeInstance function is used when the component is instantiated.

Next, we continue to look at __Patch__

The second patch.

The __Patch__ method is defined as the createPatchFunction method that is executed. This method is quite large, so let’s first look at the definition of the main entrance patch method:

return function patch(oldVnode, vnode, hydrating, removeOnly) {
  // ...
  
  const isRealElement = isDef(oldVnode.nodeType)
  if(! isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue,null.null, removeOnly)
  }else {
    if(isRealElement) {
      // SSR attributes... Temporarily ignore
      
      oldVnode = emptyNodeAt(oldVnode)
    }
    
    const oldElm = oldVnode.elm
    const parentElm = nodeOps.parentNode(oldElm)
    createElm(
      vnode, 
      insertedVnodeQueue, 
      oldElm._leaveCb ? null : parentElm, 
      nodeOps.nextSibling(oldElm)
    )
    
    if (isDef(vnode.parent)) {
      // ...
    }
  }
  
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}
Copy the code

Let’s start with 🌰 :

<html>
  <head>
    <meta charset="utf-8"/>
  </head>

  <body>
    <div id='root'></div>

    <script src=".. /vue/dist/vue.js"></script>
    <script>

      let vm = new Vue({
        el: '#root'.data() {
          return {
            a: "This is the root node."}},template: ` < div data - test = 'this is the test attribute' @ click = "handleClick" > {{a}} < / div > `.methods: {
          handleClick() {
            this.a = 'changed'}}})</script>
  </body>
</html>
Copy the code

When the page is rendered, an Update patch is performed.

oldVnode

At this point oldVnode is div#root, which is the actual DOM node.

The vnode value is obtained by executing the render function, and its structure is roughly as follows:

vnode

{
  tag: "div".text: undefined.key: undefined.isStatic: false.isRootInsert: true.isComment: false.elm: undefined.componentInstance: undefined.componentOptions: undefined.children: [
    // vnode plain text node
    {
      // ...}].context: Vue,
  data: {
    attrs: {... },on: {
      click: function () {... }}}}Copy the code

nodeType

NodeType is actually a native HTML attribute, where nodeType is a node with a value of 1 when first rendered.

Unknown nodeType friend, to: www.w3school.com.cn/jsref/prop_…

Going back to our demo, isRealElement = 1 is true. EmptyNodeAt is called at this point

emptyNodeAt

function emptyNodeAt (elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
Copy the code

The problem is that oldVnode is a real DOM node with id = “root” the first time the page is rendered. Why do I need to call the emptyNodeAt method to reset to a virtual DOM node?

There are several reasons:

    1. RemoveVnodes operates based on the virtual DOM
    1. InvokeDestroyHook is also based on virtual DOM operations
    1. Diff comparison between old and new nodes is based on virtual DOM operation

At this point, after root node is transformed into virtual DOM (namely oldVnode), its data structure is as follows:

{
  tag: "div".text: undefined.key: undefined.isStatic: false.isRootInsert: true.isComment: false.elm: undefined.componentInstance: undefined.componentOptions: undefined.children: [].context: Vue,
  data: {},
  // Notice this change
  elm: div#root
}
Copy the code

At this point, before the page is rendered, there is only an empty div with id = ‘root’, and vue converts it to a vnode. The oldVnode above it is compared to the vnode after new Vue.

It should be noted that parentElm refers to body at the first update

3. CreateElm

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  // ...
  
  // Handle nested components
  if(createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  
  // ...
  
  const tag = vnode.tag
  if(isDef(tag)) {
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode);
        
      setScope(vnode)
  
      if(__WEEX__) {
        / /... Weex related processing
      }else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
  }else if(isTrue(vnode.isComment)){
     // Comment the node
     vnode.elm = nodeOps.createComment(vnode.text)
     insert(parentElm, vnode.elm, refElm)
  }else {
    // Plain text node
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
} 
Copy the code

CreateComponent implements multiple layers of nested components, which will not be covered here. If you are not sure, you can see my previous “Vue source parsing – Componentization & virtual DOM”.

On the first rendering, the createElm method is triggered and the vNode passed in is the new vNode. Obviously, in our demo, tag = “div”, ns = undefined. The nodeOps. CreateElement method will be executed.

The nodeOps object, in effect, encapsulates operations on the native DOM. Here the createElement method, which essentially calls the document.createElement method, returns the native DOM object.

The elm property in the new virtual DOM points to the newly created div. The vNode data structure is as follows:

{
  tag: "div".text: undefined.key: undefined.isStatic: false.isRootInsert: true.isComment: false.// The elm property value is changed to the newly created DIV DOM object
  elm: div,
  componentInstance: undefined.componentOptions: undefined.children: [
    // vnode plain text node
    {
      // ...}].context: Vue,
  data: {
    attrs: {... },on: {
      click: function () {... }}}}Copy the code

Note that setScope(vnode) actually adds scopeId to the style object of elm’s real DOM.

At this point, the outer div in the demo has been created, but no text is displayed at this point. Because the text in children is also a virtual DOM, but it’s just a plain text node.

The following process calls the createChildren method.

Four createChildren.

The trunk code is as follows:

function createChildren (vnode, children, insertedVnodeQueue) {
  // ...
  
  for (let i = 0; i < children.length; ++i) {
    createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
  }
  // ...
}
Copy the code

Here, we can see that it’s actually a recursive loop. No matter how many layers are nested within our component, the Childrens of each vNode will be looped. Then createElm, childrens, continue to call createChildren. And so on, recursively creating the sub-components one by one.

In our demo, child Childrens is a line of text that is a plain text node. So at createElm, the last else operation will be performed to create the text node. The native DOM call:

document.createTextNode(text)
Copy the code

Similarly, the ELM property on the child’s VNode object points to the real DOM object of the text node you just created.

Finally, call the Update restoreActiveInstance method to activate the current parent component as the current activeInstance instance.

Well, this is just the simplest patch process that doesn’t involve multiple layers of nesting and comparison.

Since this is the first rendering process, diff is when the rendered page happens and the page needs to change again.

Next, we will enter the patch process where data changes and views need to change

Five patchVnode.

reactiveSetter

The previous section covered dependency collection, and we know that reactiveSetter is triggered when data changes.

First, the reactiveSetter checks whether the values before and after are the same. If so, return directly. Otherwise, proceed to the next section.

When collecting dependencies, the Dep class instance object Dep has a subs array containing the Watcher objects that depend on the data.

So when reactiveSetter is triggered, you are actually calling each watcher’s update method.

Watcher’s update method does not update directly. Instead, watcher is placed in an update queue.

Note: The maximum size of the update queue is 100

Finally, the nextTick function is called, the promise is set to update the queue, and the Scheduler job, the run method for each watcher, is executed in the callback. It will finally enter the second round of Patch.

Note: Why queues? It’s really two things:

  • Performance considerations. Because the same nextTick component may depend on multiple data objects, and multiple data objects change, there is no need to update multiple times. In the queue, Vue will determine whether it belongs to the same Watcher ID.
  • Multiple components, each dependent on multiple data objects. Each component actually has its own nextTick.

This is actually more than that, and I’ll have a separate section to share update queues and nextTick.

At this point, the oldVnode data structure is as follows:

oldVnode

{
  tag: "div".text: undefined.key: undefined.isStatic: false.isRootInsert: true.isComment: false.elm: div,
  componentInstance: undefined.componentOptions: undefined.children: [{tag: undefined.text: "This is the root node.".key: undefined.isStatic: false.isRootInsert: false.isComment: false.elm: test,
      componentInstance: undefined.componentOptions: undefined.children: undefined.// ...}].context: Vue,
  data: {
    attrs: {... },on: {
      click: function () {... }}},// ...
}
Copy the code

Vnode (new VNode)

{
  tag: "div".text: undefined.key: undefined.isStatic: false.isRootInsert: true.isComment: false.elm: div,
  componentInstance: undefined.componentOptions: undefined.children: [{tag: undefined.// Notice the change here
      text: "Changed".key: undefined.isStatic: false.isRootInsert: false.isComment: false.elm: test,
      componentInstance: undefined.componentOptions: undefined.children: undefined.// ...}].context: Vue,
  data: {
    attrs: {... },on: {
      click: function () {... }}},// ...
}
Copy the code

If isRealElement = false, the sameVnode judgment will be performed first.

What does sameVnode do first

sameVnode

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      )
      ||
      (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
Copy the code

The first level of judgment here is the key judgment, familiar? That’s why we need a key when we write an array loop.

The sameInputType method is very simple:

    1. If it is not an input node, return true
    1. If so, determine whether data, attrs, and type on the virtual DOM are equal

Finally, the patchVnode method is entered:

The trunk code is as follows:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
  if (oldVnode === vnode) {
    return
  } 
  
  // ...
  
  / /... Omit asynchronous placeholder components
  
  if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
  }
  
  // ...
  // The component node needs to call the component prepatch hook, data,props,slot,listener, etc
  / /... Is omitted
  
  if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if (isDef(ch)) {
        if(process.env.NODE_ENV ! = ='production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ' ')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, ' ')}}else if(oldVnode.text ! == vnode.text) { nodeOps.setTextContent(elm, vnode.text) } }Copy the code

As you can see, here is the before and after update of the virtual DOM. There are roughly the following types:

    1. Check whether oldVnode and VNode are equal. If they are equal, return
    1. If oldVnode is static and vNode is static. And oldvNode. key is equal to vnode.key. In addition, the vNode is cloned or isOnce, so directly return, no need to compare.
    1. If the new vNode is not a text node, then:
    • 3.1 If children exist in both oldVnode and vnode, then:

      • 3.1.1 If 2 children are not equal, then update echildren (complicated here, need separate analysis)
    • 3.2 If the new VNode has children but the old oldVnode does not have children, then:

      • 3.2.1 If the old oldVnode is a text node, delete the content in the real DOM first, and then add the children of the new vnode to the real DOM.

      • 3.2.2 If the old oldVnode is not a text node, add it directly to the DOM

    • 3.3 If children do not exist in the new VNode but exist in the old oldVnode, clear the dom of children directly

    • 3.4 If neither the new VNode nor the old oldVnode has children, but the old oldVnode is a text node, then the DOM content is directly emptied

    1. If the new vNode is a text node and the old oldVnode is a text node, then: If the contents are not equal, the old content is overwritten with the new content

updateChildren

Above, in the 3.1.1 case, if children exist in both old and new VNodes, but they are not equal, the updateChildren method is called. This is explained separately.

In fact, there are no more than four ways to deal with children, which are:

    1. Creating child Nodes
    1. Deleting child Nodes
    1. Move child node
    1. Update child nodes

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let 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, idxInOld, vnodeToMove, refElm

    constcanMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1])?null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
Copy the code

As you can see, the updateChildren phase is actually a double loop between the new VNode and the old oldVnode.

As follows:

start

The second step

The third step

The fourth step

Step 5

Step 6

Diff comparisons, in fact, are based on the new VNode, with the old vNode constantly adjusted.

You can see Diff’s comparison strategy, left left, right right, right left, right left

6. Summary

Patch Phase 1: activeInstance

    1. Only one component is instantiating and patching at a time
    1. Sets the activeInstace object that is currently being instantiated and keeps preActiveInstace.
    1. The currently reserved activeInstace should be instantiated as the parent of a nested component encountered during patch.
    1. After the patch of the current subcomponent is completed, the activeInstance is changed to preActiveInstance. If there are multiple layers of nesting, the process is repeated.

Patch Phase 2: Root DOM patch

    1. The dom root is the div#root container, and oldVnode is not a virtual dom but a real dom when the page page is rendered. Oldvnode. nodeType = 1
    1. Convert div#root itself into an empty virtual dom. Compare it as an oldVnode with the new VNode.
    1. Enter createElm, then:
    • 3.1 If there are components, componentize patch (componentize can be seen in my previous article for those who are not familiar with componentize)
    1. After the insert, note that patchVnode Diff is not entered.
    1. Set the DOM Style scope ID

Patch stage 3: reactiveSetter

    1. The page is rendered for the first time, and reactiveSetter is triggered if data changes on the page. (For those who are not clear about the collection, please see my previous share: “Vue source parsing – Responsive Principle”)
    1. Check whether the old and new data are equal. If so, return
    1. If the data is not equal, subs under the root DEP will be looped through watcher’s update method.
    1. Watcher update is not a direct notification of updates. I put them in a queue. Update notifications go to queueWatcher
    1. QueueWatcher optimizes unnecessary multiple renders, such as multiple value changes pointing to the same watcher, without triggering multiple patches
    1. Watcher’s run method is called in the Scheduler job
    1. Execute the render function to get a new VNode, perform update, and repeat the previous stage.
    1. IsRealElement is undefined, and the patchVnode stage is entered

Patch Phase 4: patchVnode

    1. Compare new and old nodes to see if they are equal. OldVnode == vnode. If so, return
    1. Whether the node is static, whether the keys are the same, or whether the node is a clone /isOnce. Return directly. (Note: Static tags are generated in Compiler stage 2)
    1. Update oldVnode props, Listener, slots, parent, and so on according to vNode
    1. The new vNode is a text node, then:
    • 4.1 If both oldVnode and VNode have Childrens,
      • 4.1.1 If 2 children are equal, return directly
      • 4.1.2 If two children are not equal, then only phase 5 – Update echildren is required
    • 4.2 If the new node has children but the old node does not, then:
      • 4.2.1 If the old node is a text node, clear the content of the old child node first
      • 4.2.1 Insert multiple children of the new VNode into the old DOM stream
    • 4.3 If the new node does not have children and the old node has children, then:
      • 4.3.1 Delete all the old Childrens
    • 4.4 If there are no children in the old node and the new node, and the old node is a text node, clear the content of the old node
    1. The new and old nodes are text nodes, but the content of the file node is different, so the old text content is directly updated with the new text content

Patch Phase 5: Update dren

    1. Nodes of different layers cannot be reused for same-layer comparison
    1. OldStartVnode refers to the unprocessed start node, newStartVnode is the new unprocessed start node
    1. OldEndVnode refers to the unprocessed last node, newEndVnode new unprocessed last node
    1. OldStartVnode and newStartVnode are compared first, then:
    • 4.1 If they are equal, move oldStartVnode and newStartVnode one later
    • 4.2 If not, enter oldEndVnode and newEndVnode to compare
    1. OldEndVnode is compared with newEndVnode, then:
    • 5.1 If they are equal, move oldEndVnode and newEndVnode forward by one
    • 5.2 If not, enter oldStartVnode and newEndVnode
    1. OldStartVnode is compared with newEndVnode, then:
    • 6.1 If they are equal, move oldStartVnode back one and newEndVnode forward one.
    • 6.2 If they are not equal, oldEndVnode and newStartVnode are compared
    1. OldEndVnode is compared with newStartVnode, then:
    • 7.1 If they are equal, move oldEndVnode forward and newStartVnode forward
    • 7.2 If it is not equal, the search node will be entered
    1. Based on the new VNode location, go to the old node in the same layer.
    • 8.1 If so, move to the corresponding position (note that it is the unprocessed node as the reference, not the processed node)
    • 8.2 If the node does not exist, create a new node according to children and add it to the old node
    • 8.3 If the old node does not exist in the new node, delete the old node
    1. That’s the two-pointer algorithm, so you loop through, and you compare all the nodes. In general, there are three points:
    • 9.1 The same layer does not exist. Update and move
    • 9.2 If the same layer does not exist, create one
    • 9.3 New node, the same layer does not exist in the old node, then delete

The above is the general process of patch.

Code word is not easy, pay more attention to 😽