Process review of data update

During component instance initialization, the Watcher instance is associated with updateComponent.

letUpdateComponent = () => {// Updates the Component definition to do two things: Vm._update (vm._render(), hydrating)}Copy the code

Focus on vm._update(). View/core/instance/lifecycle. Js:

Vue.prototype._update = function(vnode: VNode, hydrating? : boolean) { ... / / to omitif(! PrevVnode) {// Initialize the render VM.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el= vm.__patch__(prevVnode, vnode) } ... / / omit}Copy the code

The vm here. __patch__ () is the entrance of the source stage platforms/web/runtime/index defined in js:

import { patch } from './patch'// Define the patch function, which is the operation that converts the virtual DOM to the real DOM, very important Vue. Prototype. __patch__ =inBrowser ? patch : noop
Copy the code

Into the platforms/web/runtime/patch. The js:

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'Const modules = platformmodules. concat(baseModules) // Introduce platform-specific codes, where nodeOps are node operations and modules are attribute operationsexport const patch: Function = createPatchFunction({ nodeOps, modules })
Copy the code

The two parameters in createPatchFunction({nodeOps, modules}) nodeOps and modules are platform-specific code. Among them:

NodeOps are node operations that define various native DOM base operations. Modules are property operations that define property update implementations.

CreatePatchFunction is in the file core/vdom/patch.js. This file is the largest file in the whole Vue project and records the patch process in detail. Now let’s have a detailed look at the principle and implementation of patch algorithm.

Patch algorithm

The patch algorithm compares the old and new VNode trees and modiates the view in the smallest unit according to the comparison results (the origin of the patch), rather than redrawing the whole view according to the new VNode. The core of Patch lies in diff algorithm.

Diff algorithm uses tree nodes of the same layer to compare (instead of searching the whole tree layer by layer), with only O(n) time complexity, which is a fairly efficient algorithm. This is illustrated in the following diagram: Between two trees, squares of the same color represent vNodes that are being compared with each other.

patch()

Check the patch algorithm source code:

return functionPatch (oldVnode, vnode, hydrating, removeOnly) {// New node does not exist, old node exists, remove the old node directlyif (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = trueCreateElm (vnode, insertedVnodeQueue)}elseConst isRealElement = isDef(oldvNode.nodeType) // Is not a real DOM and is the same VNode elementif(! IsRealElement &&samevNode (oldVnode, vnode) {// Update patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) }else{// Real DOM, that is, initialization logicif (isRealElement) {
          
          if(oldvnode.nodetype === 1&&oldvnode.hasattribute (SSR_ATTR)) {// when the oldVnode is an element rendered by the server, hydrating is denoted astrue
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if(isTrue(hydrating)) {// Need to merge server rendered pages into the real DOMif (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              returnOldVnode}} // Not a server render node, or the server render merge to the real DOM failed, OldVnode = emptyNodeAt(oldVnode)} // Replace the existing element const oldElm = oldvNode. elm const parentElm = ParentNode (oldElm) createElm(vnode, insertedVnodeQueue, oldelm._leavecb? Null: parentElm, // Specify the parent nodeops.nextsibling (oldElm) // specify the insertion location: next to the existing element)... // omit // remove the old nodeif (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
Copy the code

The logic of the above code is not difficult, mainly for the same layer of the tree node comparison. The operations include adding a new node, deleting an old node, and updating a node.

Checking the source code, it is found that patchVnode() will be updated only when the old and new nodes are judged to be the same node. What is the same node? Let’s look at sameVnode() :

sameVnode()

/* To check whether two vNodes are the same node, the following conditions must be met: IsComment (whether it is a comment node) Yes No Data (The object corresponding to the current node contains specific data information. It is of the VNodeData type. When the tag <input>,typeMust be the same */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) ) ) ) } /* Check when the tag is <input>,typeAre they the same Some browsers do not support dynamic modification of <input> types, so they are treated as different types */function sameInputType (a, b) {
  if(a.tag ! = ='input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
Copy the code

If the tag, key, and isComment of two VNodes are the same, and data is defined or not, and if the label is input, the type must be the same. In this case, the two VNodes are considered as Samevnodes, and patchVnodes can be directly operated.

patchVnode()

  functionPatchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {// If two vNodes are identical, the patchVnode is returnedif (oldVnode === vnode) {
      return} const elm = vnode.elm = oldVnode.elm ... // omit /* If both the old and new vNodes are static, they have the same key (representing the same node), and the new VNode iscloneOr mark once (mark the V-once attribute and render only once), then just replace componentInstance. * /if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if(isDef(data) &&isdef (I = data.hook) &&isdef (I = i.patch)) {// I = data.hook"./create-component componentVNodeHooks"
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if(isDef(data) &&isPatchable (vnode)) {// Call update callback and update hookfor (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if(isDef(I = data.hook) &&isdef (I = i.date)) I (oldVnode, vnode)} // The text attribute of vnode is exclusive with the children attributeif (isUndef(vnode.text)) {
      
      if(isDef(oldCh) &&isdef (ch)) {// The old has children, and the new has children, and diff the childrenif(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if(isDef(ch)) {// The new node has children, the old node has no children, first empty the text of the old nodeif(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 ! Node.settextcontent (elm, vnode.text) == vnode.text) {nodeops.settextContent (elm, vnode.text)}if(isDef(data)) {// I = data.hook.postpatch"./create-component componentVNodeHooks"
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
Copy the code

The code is not hard to understand. The rules of patchVnode are as follows:

1. If the old and new VNodes are static, have the same key (representing the same node), and the new VNode is clone or once (marked with the V-once attribute, rendering only once), then only elm and componentInstance need to be replaced.

2. If both the old and new nodes have children, diff is performed on the children and updateChildren is called, which is also the core of the diff.

3. If the old node has no children and the new node has children, clear the text content of the DOM of the old node and add children to the current DOM node.

4. When a new node has no children and an old node has children, remove all children of the DOM node.

5. When the old and new nodes have no children, it is just a text replacement.

updateChildren()

  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

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay incorrect relative positions // during leaving transitions const canMove = ! 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] /* Then directly patchVnode can compare two nodes of oldCh and newCh: 2*2=4 */}else if(sameVnode(oldStartVnode, newStartVnode)) {// patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }else ifPatchVnode (oldEndVnode, newEndVnode) {// patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }else ifPatchVnode (oldStartVnode, newEndVnode) {// patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, CanMove && nodeops. insertBefore(parentElm, oldStartVNode. elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else ifPatchVnode (oldEndVnode, newStartVnode) {// patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, CanMove && nodeops. insertBefore(parentElm, oldEndVNode. elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }else{// In OldCh, create a map table for key<==> idX to speed up the search for newStartVnode in OldChif (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if(isUndef(idxInOld)) {// newStartVnode is a new element if it does not exist in OldCh. CreateElm (newStartVnode, insertedVnodeQueue, parentElm, oldStartvNode. elm,false, newCh, newStartIdx)
        } else{vnodeToMove = oldCh[idxInOld] /* Compare the vnodes found in oldCh with newStartVnode with the same key. Check whether the vnodes are the same. Same tag isComment same data is defined when the tag is <input>,typeMust be the same */if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } elseCreateElm (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 ifRemoveVnodes (oldCh, oldStartIdx, oldEndIdx)}} (newStartIdx > newEndIdx) {// If newCh has been traversed and oldCh has elements, remove the remaining elements.Copy the code

The process is not complicated. It’s a little bit clearer when you draw it.

Four simple comparisons

There is a variable mark on both sides of the left and right sides of the old and new VNodes, which converge towards the middle during traversal. The loop ends when oldStartIdx <= oldEndIdx or newStartIdx <= newEndIdx. First, oldStartVnode, oldEndVnode, newStartVnode, newEndVnode pairwise comparison, there are 2*2=4 comparison methods.

Equal heads or equal tails

When the start or end of the old and new Vnodes meets sameVnode(), patchVnode can be used directly.

Equal to the end

If oldStartVnode and newEndVnode satisfy sameVnode(), that is sameVnode(oldStartVnode, newEndVnode). OldStartVnode is running after oldEndVnode. At the same time of patchVnode, oldStartVnode needs to be moved to the end of oldEndVnode.

End is equal

If oldEndVnode and newStartVnode satisfy sameVnode(), that is sameVnode(oldEndVnode, newStartVnode). This indicates that oldEndVnode is ahead of oldStartVnode. While patchVnode() is being performed, oldEndVnode needs to be moved to the front of oldStartVnode.

Non-simple comparison

If the above four simple conditions are not met, create a map table of key<==> IDX in OldCh to speed up the search for newStartVnode in OldCh. From this map table, you can find whether there are old vNodes with the same key as newStartVnode. If sameVnode() is satisfied, patchVnode() simultaneously moves the real DOM (elmToMove) in front of the real DOM corresponding to oldStartVnode.

Of course, it’s nice to have the same node, but more often it’s not: the key is different, or the key is the same but still doesn’t satisfy sameVnode(). In this case, you need to create a new node:

And, of course, there are termination conditions.

If the oldCh array has been traversed and the newCh array still has elements, the remaining elements are created.

Patch Algorithm Review

The whole patch algorithm is very long. The whole down, will have a more profound understanding of its implementation process.

The next question I want to ask is: why are there four ways to compare heads to tails when updating echildren ()? Does it do any good?

We might drag and drop one DOM element and leave the rest unchanged. Or click a button to reverse the order of presentation elements; Or a DOM element is cleared.

These operations simply change the hierarchy of the DOM tree. A head-to-tail comparison is perfectly effective in dealing with these daily operations.

This can also be an engineering – oriented algorithm optimization case ~

MORE

As for DOM operation update, patch algorithm has shown us the whole process. But there are still details to be worked out: the patch process simply maps the virtual DOM to the real DOM. How do you add attr, class, style, and other DOM attributes to the DOM?

In fact, the source code already has the answer, just not detailed enough. Its implementation depends on VDOM’s lifecycle functions. In createPatchFunction(), there is this paragraph:

//VDOM lifecycle hook function const hooks = ['create'.'activate'.'update'.'remove'.'destroy'] // Introduce DOM attribute-related lifecycle hook functionsfor(i = 0; i < hooks.length; + + I) {CBS [hooks [I]] = [] / / modules include [attrs, klass, events, domProps, style, transition, ref, directives] such as DOM attributesfor (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
Copy the code

Where is this function in the CBS array being called? One line of code in patchVNode() that is easily overlooked:

    if(isDef(data) &&isPatchable (vnode)) {// Call the update hook function associated with each DOM attributefor (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
Copy the code

This is where the corresponding DOM attribute is formally updated. In fact, it means that in the process of patchVNode(), the corresponding update of DOM attribute has been quietly completed.


Vue source code interpretation

(1) : Vue constructor and initialization process

(II) : Data response and implementation

(3) : array responsive processing

(iv) Vue’s asynchronous update queue

(5) The introduction of virtual DOM

(VI) Data update algorithm — Patch algorithm

(7) : realization of componentization mechanism

(8) : computing properties and listening properties

The Optimize stage of the compilation process

Vue

Vue’s error handling mechanism

Parse the working process of Vue in handwritten code

Vue Router handwriting implementation