Interpretation of Vue source code (12) -- patch

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

preface

As mentioned earlier, when the component is updated, the updateComponent method passed when instantiating the render watcher will be executed:

const updateComponent = () => {
  // Execute VM_ Render() function, get the virtual VNode, and pass the VNode to VM_ Update method, and then it's time to go to the patch stage
  vm._update(vm._render(), hydrating)
}

First, VM_ Render() function, get the VNode of the component, and pass the VNode to VM_ Update method, and then it's time to enter the patch phase. Today, let's have an in-depth understanding of the execution process of patch when updating components.

history

The 1.x version of Vue does not have VNode and diff algorithms. The core of that version of Vue is only the responsive principle: object defineProperty,Dep,Watcher.

  • Object.defineProperty: responsible for data interception. The dependency collection is performed during getter and dep notifies the watcher to update during setter
  • dep: the object returned by Vue data option. The key and dep of the object correspond to each other one by one
  • Watcher: there is a one to many relationship between key and watcher. Every time a key is used in the component template, a Watcher will be generated
<template>
  <div class="wrapper">
    <!-- Each time responsive data is referenced in the template, a watcher -->
    <!-- watcher 1 -->
    <div class="msg1">{{ msg }}</div>
    <!-- watcher 2 -->
    <div class="msg2">{{ msg }}</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // One to one correspondence with dep and one to many correspondence with watcher
      msg: 'Hello Vue 1.0'
    }
  }
}
</script>

When the data is updated, dep informs the watcher to directly update the dom. Because this version of the watcher and the DOM have a one-to-one correspondence, the watcher can clearly know the position of the key in the component template, so it can achieve directional update, so its update efficiency is very high.

Although the update efficiency is high, it also produces serious problems, which makes it impossible to complete an enterprise application. The reason is very simple: when your page is complex enough, it will contain many components. Under this architecture, it means that this page will produce a large number of watcher s, which is very resource consuming.

At this time, Vue 2.0 introduces VNode and diff algorithm to solve 1 Problems in X. Enlarge the granularity of the watcher and turn it into a component into a watcher (that is, the rendering watcher). At this time, no matter how large your page is, there are few watchers, which solves the problem of performance degradation caused by too many watchers on complex pages.

When the responsive data is updated, dep informs the watcher to update. At this time, the problem comes. Vue 1 The watcher and key in X correspond one by one, so you can know exactly where to update. However, the watcher in Vue 2.0 corresponds to an entire component, and the watcher does not know where the updated data is in the component. At this time, VNode needs to solve the problem.

By introducing VNode, when the data in the component is updated, a new VNode will be generated for the component. By comparing the new and old vnodes, find out the differences, and then execute DOM operation to update the changed node. This process is known as diff.

The above is the historical reason why Vue 2.0 introduces VNode and diff algorithms, and also Vue 1.0 X to 2 A development of X.

target

  • Deeply understand Vue's patch stage and the principle of its diff algorithm.

Source code interpretation

entrance

/src/core/instance/lifecycle.js

const updateComponent = () => {
  // Execute VM_ Render() function, get VNode and pass VNode to_ update method, and then it's time to go to the patch stage
  vm._update(vm._render(), hydrating)
}

vm._update

/src/core/instance/lifecycle.js

/**
 * The entry location of the first rendering and subsequent updates of the page is also the entry location of the patch 
 */
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  // Page mount point, real element
  const prevEl = vm.$el
  // Old VNode
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  // New VNode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // The old VNode does not exist, which means that it is rendered for the first time, that is, when initializing the page
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // When the responsive data is updated, that is, when the page is updated, go here
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

vm.\_\_patch\_\_

/src/platforms/web/runtime/index.js

/ stay Vue Installation on prototype chain web Platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch

/src/platforms/web/runtime/patch.js

// Patch factory function, pass in some platform specific operations for it, and then return a patch function
export const patch: Function = createPatchFunction({ nodeOps, modules })

nodeOps

src/platforms/web/runtime/node-ops.js

/**
 * web DOM operation API of platform
 */

/**
 * Create an element node with the tag name tagName
 */
export function createElement (tagName: string, vnode: VNode): Element {
  // Create element node
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  // If it is a select element, set the multiple attribute for it
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}

// Create element node with namespace
export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

// Create text node
export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

// Create annotation node
export function createComment (text: string): Comment {
  return document.createComment(text)
}

// Insert node before specifying node
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

/**
 * Remove the specified child node
 */
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

/**
 * Add child node
 */
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

/**
 * Returns the parent node of the specified node
 */
export function parentNode (node: Node): ?Node {
  return node.parentNode
}

/**
 * Returns the next sibling node of the specified node
 */
export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}

/**
 * Returns the label name of the specified node 
 */
export function tagName (node: Element): string {
  return node.tagName
}

/**
 * Sets the text for the specified node 
 */
export function setTextContent (node: Node, text: string) {
  node.textContent = text
}

/**
 * Set the specified scopeId attribute for the node with the attribute value ''
 */
export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

modules

/src/platforms/web/runtime/modules and / src/core/vdom/modules

Some platform specific operations, such as attr, class, style, event, and the core directive and ref, will expose some specific methods, such as create, activate, update, remove, destroy. These methods will be called in the patch phase to perform corresponding operations, such as creating attr and instructions. There are too many contents in this part, so I won't list them one by one here. You can go back and read in depth if necessary during the process of reading patch. For example, when operating the attributes of nodes, read the code related to attr.

createPatchFunction

Tip: due to the large amount of code of this function, the code structure has been adjusted to facilitate reading and understanding

/src/core/vdom/patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

/**
 * Factory function, inject some function operations unique to the platform, define some methods, and then return the patch function
 */
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  /**
   * modules: { ref, directives, Some platform specific manipulations, such as attr, class, style, etc.}
   * nodeOps: { API}
   */
  const { modules, nodeOps } = backend

  /**
   * hooks = ['create', 'activate', 'update', 'remove', 'destroy']
   * Traverse these hooks, and then find the corresponding methods from each module of modules, such as create, update and destroy methods in directives
   * Put these methods into cb[hook] = [hook method], for example: CB create = [fn1, fn2, ...]
   * Then call the corresponding hook method at the appropriate time to complete the corresponding operation
   */
  for (i = 0; i < hooks.length; ++i) {
    // Like CBS create = []
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // Traverse each module, find the Create method in each module, and then add it to CBS In the create array
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  /**
   * vm.__patch__
   *   1,If the new node does not exist and the old node exists, call destroy to destroy the old node
   *   2,If oldVnode is a real element, it means rendering for the first time, creating a new node, inserting the body, and then removing the old node
   *   3,If oldVnode is not a real element, it indicates the update phase and executes patchVnode
   */
  return patch
}

patch

src/core/vdom/patch.js

/**
 * vm.__patch__
 *   1,If the new node does not exist and the old node exists, call destroy to destroy the old node
 *   2,If oldVnode is a real element, it means rendering for the first time, creating a new node, inserting the body, and then removing the old node
 *   3,If oldVnode is not a real element, it indicates the update phase and executes patchVnode
 */
function patch(oldVnode, vnode, hydrating, removeOnly) {
  // If the new node does not exist and the old node exists, call destroy to destroy the old node
  if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }

  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // The new VNode exists and the old VNode does not exist. This situation will occur when a component is rendered for the first time, such as:
    // <div id="app"><comp></comp></div>
    // The comp component here will go here when rendering for the first time
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // Determine whether oldVnode is a real element
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // If it is not a real element, but the old node and the new node are the same node, it is the update phase and execute patch to update the node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      // If it is a real element, it indicates the first rendering
      if (isRealElement) {
        // Mount to real elements and process server-side rendering
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }
        if (isTrue(hydrating)) {
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            invokeInsertHook(vnode, insertedVnodeQueue, true)
            return oldVnode
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'The client-side rendered virtual DOM tree is not matching ' +
              'server-rendered content. This is likely caused by incorrect ' +
              'HTML markup, for example nesting block-level elements inside ' +
              '<p>, or missing <tbody>. Bailing hydration and performing ' +
              'full client-side render.'
            )
          }
        }
        // Go here to explain that it is not the server-side rendering, or if the hydration fails, create a vnode node according to oldVnode
        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }

      // Get the real element of the old node
      const oldElm = oldVnode.elm
      // Get the parent element of the old node, that is, body
      const parentElm = nodeOps.parentNode(oldElm)

      // Create a whole DOM tree based on the new vnode and insert it under the body element
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // Update parent placeholder node element recursively
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // Remove old nodes
      if (isDef(parentElm)) {
        removeVnodes([oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

invokeDestroyHook

src/core/vdom/patch.js

/**
 * Destroy node:
 *   Execute the destroy hook of the component, that is, execute the $destroy method 
 *   Execute the destroy method of each module (style, class, directive, etc.) of the component
 *   If the vnode still has child nodes, invokeDestroyHook is called recursively
 */
function invokeDestroyHook(vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

sameVnode

src/core/vdom/patch.js

/**
 * Judge whether the two nodes are the same 
 */
function sameVnode (a, b) {
  return (
    // The key s must be the same. Note that undefined = = = undefined = > true
    a.key === b.key && (
      (
        // Same label
        a.tag === b.tag &&
        // Are annotation nodes
        a.isComment === b.isComment &&
        // All have data attribute
        isDef(a.data) === isDef(b.data) &&
        // input tag
        sameInputType(a, b)
      ) || (
        // Asynchronous placeholder node
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

emptyNodeAt

src/core/vdom/patch.js

/**
 * Create an empty vnode for the element (elm)
 */
function emptyNodeAt(elm) {
  return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

createElm

src/core/vdom/patch.js

/**
 * Create a whole DOM tree based on vnode and insert it into the parent node
 */
function createElm(
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  /**
   * a key
   * 1,If vnode is a component, execute the init hook, create a component instance and mount it,
   *   Then execute the create hook of each module for the component
   *   If the component is wrapped by keep alive, activate the component
   * 2,If it's a normal element, nothing is bad
   */
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  // Get data object
  const data = vnode.data
  // All child nodes
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // Unknown tag
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    // Create a new node
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    // Recursively create all child nodes (common elements, components)
    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }
    // Insert node into parent node
    insert(parentElm, vnode.elm, refElm)

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    // Annotation node, create annotation node and insert parent node
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    // Text node, create a text node and insert the parent node
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

createComponent

src/core/vdom/patch.js

/**
 * If vnode is a component, execute init hook, create component instance and mount it
 * Then execute the create method of each module for the component
 * @param {*} vnode Component new vnode
 * @param {*} insertedVnodeQueue array
 * @param {*} parentElm oldVnode Parent node of
 * @param {*} refElm oldVnode Next sibling node of
 * @returns If vnode is a component and the component is created successfully, return true; otherwise, return undefined
 */
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
  // Get vnode Data object
  let i = vnode.data
  if (isDef(i)) {
    // Verify whether the component instance already exists & & wrapped by keep alive
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    // Execute vnode data. Init hook function, which was mentioned when talking about render helper
    // If it is a component wrapped by keep alive: execute the prepatch hook and update the relevant attributes on oldVnode with the attributes on vnode
    // If the component is not wrapped by keep alive or rendered for the first time, initialize the component and enter the mount phase
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      // If vnode is a sub component, a component instance will be created and mounted after calling the init hook
      // At this time, you can execute the create hook of each module for the component
      initComponent(vnode, insertedVnodeQueue)
      // Insert the DOM node of the component into the parent node
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        // When the component is wrapped by keep alive, activate the component
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

insert

src/core/vdom/patch.js

/**
 * Insert node to parent node 
 */
function insert(parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (nodeOps.parentNode(ref) === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}

removeVnodes

src/core/vdom/patch.js

/**
 * Removes nodes within the specified index range (startIdx -- endIdx) 
 */
function removeVnodes(vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else { // Text node
        removeNode(ch.elm)
      }
    }
  }
}

patchVnode

src/core/vdom/patch.js

/**
 * Update node
 *   Full attribute update
 *   If both old and new nodes have children, diff is executed recursively
 *   If the new node has children and the old node has no children, these child nodes of the new node will be added
 *   If the old node has children and the new node has no children, these children of the old node will be deleted
 *   Update text node
 */
function patchVnode(
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // The old node is the same as the new node, which is returned directly
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm

  // Asynchronous placeholder node
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // Skip updates to static nodes
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    // If the old and new nodes are static, and the key s of the two nodes are the same, and the new node is clone d or the new node has a v-once instruction, this part of the nodes will be reused
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  // Execute the prepatch hook of the component
  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  // Children of old nodes
  const oldCh = oldVnode.children
  // Children of new nodes
  const ch = vnode.children
  // Fully update the attributes of new nodes. Vue 3.0 has done a lot of optimization here
  if (isDef(data) && isPatchable(vnode)) {
    // Perform all attribute updates for the new node
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  if (isUndef(vnode.text)) {
    // The new node is not a text node
    if (isDef(oldCh) && isDef(ch)) {
      // If both old and new nodes have children, the diff process is executed recursively
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // If the old child does not exist and the new child exists, create these new child nodes
      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)) {
      // If the old child exists and the new child does not exist, remove these old child nodes
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // If the old node is a text node, leave the text content blank
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // If the new node is a text node, the text node is updated
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

updateChildren

src/core/vdom/patch.js

/**
 * diff Process:
 *   diff Optimization: four assumptions are made. It is assumed that there are the same nodes at the beginning and end of the new and old nodes. Once the assumption is hit, a cycle is avoided to improve the execution efficiency
 *             If the assumption is unfortunately missed, the traversal is performed to find the new start node from the old node
 *             If the same node is found, execute patchVnode, and then move the old node to the correct position
 *   If the old node traverses before the new node, the remaining new nodes perform the operation of adding new nodes
 *   If the traversal of the new node ends before the old node, the remaining old nodes will be deleted to remove these old nodes
 */
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  // Start index of old node
  let oldStartIdx = 0
  // Start index of new node
  let newStartIdx = 0
  // End index of old node
  let oldEndIdx = oldCh.length - 1
  // First old node
  let oldStartVnode = oldCh[0]
  // Last old node
  let oldEndVnode = oldCh[oldEndIdx]
  // End index of the new node
  let newEndIdx = newCh.length - 1
  // First new node
  let newStartVnode = newCh[0]
  // Last new node
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by < transition group > to ensure that the removed elements remain in the correct relative position during the transition
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    // Check whether the key of the new node is duplicate
    checkDuplicateKeys(newCh)
  }

  // Traverse the new and old groups of nodes. As long as one group is traversed (the start index exceeds the end index), it will jump out of the loop
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // If the node is moved, it may not exist on the current index. Detect this situation. If the node does not exist, adjust the index
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // The old start node and the new start node are the same node. Execute patch
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // After the patch ends, the indexes of the old start and the new start are added by 1 respectively
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // The old end and the new end are the same node, and the patch is executed
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // After the patch ends, the indexes of the old end and the new end are reduced by 1 respectively
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // The old start and the new end are the same node, and the patch is executed
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // Used when handling components wrapped by transition group
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      // After the patch ends, the old start index increases by 1 and the new end index decreases by 1
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // The old end and the new start are the same node, and the patch is executed
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      // After the patch is completed, the old ending index is reduced by 1 and the new beginning index is increased by 1
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // If the above four assumptions are not true, the location index of the new start node in the old node can be found through traversal

      // Find the relationship mapping between the key and index of each node in the old node = > oldkeytoidx = {key1: idx1,...}
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      // Find the location index of the new start node in the old node in the mapping
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        // If the new start node is not found in the old node, it indicates that it is a newly created element, and the creation is executed
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        // Found the new start node in the old node
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // If the two nodes are the same, execute patch
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          // After the patch is completed, set the old node to undefined
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // Finally, if the node is found, but the two nodes are not the same node, it will be regarded as a new element and created
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      // Move the old node backward one
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // Here, it means that the old sister node or the new node has been traversed
  if (oldStartIdx > oldEndIdx) {
    // It indicates that the old node has been traversed and the new node has a surplus, it indicates that the remaining nodes in this part are new nodes, and then add these nodes
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    // It indicates that the new node has been traversed and the old node has a surplus. It indicates that this part of the node has been deleted, then remove these nodes
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

checkDuplicateKeys

src/core/vdom/patch.js

/**
 * Check whether the key s of a group of elements are repeated 
 */
function checkDuplicateKeys(children) {
  const seenKeys = {}
  for (let i = 0; i < children.length; i++) {
    const vnode = children[i]
    const key = vnode.key
    if (isDef(key)) {
      if (seenKeys[key]) {
        warn(
          `Duplicate keys detected: '${key}'. This may cause an update error.`,
          vnode.context
        )
      } else {
        seenKeys[key] = true
      }
    }
  }
}

addVnodes

src/core/vdom/patch.js

/**
 * Adds a node within the specified index range (startIdx -- endIdx)
 */
function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  for (; startIdx <= endIdx; ++startIdx) {
    createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
  }
}

createKeyToOldIdx

src/core/vdom/patch.js

/**
 * Get the relationship mapping between the key and index of the node in the specified range (beginIdx -- endIdx) = > {key1: idx1,...}
 */
function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key
  const map = {}
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key
    if (isDef(key)) map[key] = i
  }
  return map
}

findIdxInOld

src/core/vdom/patch.js

/**
  * Find the location index of the new node (vnode) in the old node (oldCh) 
  */
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    if (isDef(c) && sameVnode(node, c)) return i
  }
}

invokeCreateHooks

src/core/vdom/patch.js

/**
 * Call the create method of each module, such as creating attributes, creating styles, instructions, etc., and then execute the mounted life cycle method of the component
 */
function invokeCreateHooks(vnode, insertedVnodeQueue) {
  for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode)
  }
  // Component hook
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    // The component does not seem to have a create hook
    if (isDef(i.create)) i.create(emptyNode, vnode)
    // Call the insert hook of the component and execute the mounted life cycle method of the component
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}

createChildren

src/core/vdom/patch.js

/**
 * Create all child nodes and insert the child nodes into the parent node to form a DOM tree
 */
function createChildren(vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    // children is an array, representing a group of nodes
    if (process.env.NODE_ENV !== 'production') {
      // Check whether the key s of this group of nodes are duplicate
      checkDuplicateKeys(children)
    }
    // Traverse this group of nodes, create these nodes in turn, and then insert the parent node to form a DOM tree
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    // Description is a text node. Create a text node and insert the parent node
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

summary

  • The interviewer asked: can you talk about Vue's patch algorithm?

    Answer:

    Vue's patch algorithm has three functions: it is responsible for the first rendering and subsequent updating or destroying components

    • If the old VNode is a real element, it means rendering for the first time, creating the whole DOM tree, inserting the body, and then removing the old template node
    • If the vpat element does not exist, it indicates that the vpat element is not a new one, and if the update element is not a new one

      • The first is to update all attributes in full
      • If both old and new vnodes have children, update children is executed recursively to perform the diff process

        According to the characteristics of front-end operation DOM node, the following optimization is carried out:

        • Same layer comparison (reduce time complexity) depth first (recursion)
        • Moreover, the front end rarely completely disrupts the order of nodes, so four assumptions are made. It is assumed that there are the same nodes at the beginning and end of new and old vnodes. Once the assumption is hit, a cycle is avoided, the time complexity of diff is reduced, and the execution efficiency is improved. If the assumption is unfortunately missed, the traversal is performed to find the starting node of the new VNode from the old VNode
        • If the same node is found, execute patchVnode, and then move the old node to the correct position
        • If the old VNode traverses before the new VNode, the remaining new vnodes perform the operation of adding new nodes
        • If the traversal of the new VNode ends before the old VNode, the remaining old vnodes execute the delete operation to remove these old nodes
      • If the new VNode has children and the old VNode has no children, these new child nodes will be added
      • If the old VNode has children and the new VNode has no children, delete these old child nodes
      • The remaining one is to update the text node
    • If the new VNode does not exist and the old VNode exists, call destroy to destroy the old node

Well, here, the Vue source code interpretation series is over. If you seriously read the whole series of articles, I believe you are quite familiar with the Vue source code. No matter from the macro level or the detailed explanation of some details, there should be no problem. Even though some details are not clear now, when you encounter a problem, you can see at a glance where to go to the source code to find the answer.

Here you can try to retell the whole execution process of Vue in your mind. The process is very important, but the summary is the final sublimation moment. If you get stuck in any link, you can go back and read the corresponding part.

Remember the goals mentioned in the first article in the series? I believe that after reading it several times, you can write in your resume: proficient in the source code principle of Vue framework.

Next, we will start Vue's handwriting series.

link

Thank you for your attention, likes, collections and comments. See you next time.

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention, likes, collections and comments.

The new video and articles will be sent to WeChat official account for the first time. Li Yongning

The article has been included in github warehouse liyongning/blog , welcome to Watch and Star.

Keywords: Javascript Front-end ECMAScript TypeScript Vue.js

Added by Axelbrook on Wed, 09 Mar 2022 05:47:10 +0200