Vue source notes data driven update

Keywords: Javascript Vue

Vue version: 2.5.17-beta.0

In " Vue source notes - data driven - Mount Vue instance >As mentioned at the end of the article, the Vue instance is finally mounted to the final dom through VM. Update (VM. Render(), drafting)< Vue source notes - data driven - render >To record the VM. ﹐ render() method in, call vm.$createElement to return vnode data, and then record the normal node to call VM. ﹐ update process.

In the following example, a normal node is passed in for rendering and mounting to the dom:

new Vue({
  el: '#app',
  data () {
    return {
      message: 'Hello Vue!'
    }
  },
  render (h) {
    return h('div', {
      attrs: { id: 'app' }
    }, this.message)
  }
})

The VM. Update prototype method is in the src/core/instance/lifecycle.js file. The source code is as follows:

export let activeInstance: any = null

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    // Record the current vm instance
    activeInstance = vm
    vm._vnode = vnode // Render vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      // First render incoming real dom
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // 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.
}

As can be seen from the above source code, three variables prevl, prevVnode and prevActiveInstance are defined first, but prevlode and prevActiveInstance are empty when rendering for the first time, prevl = vm.$el is the real dom node (vm.$el defined in the mountComponent method), then active instance is defined as the current instance, and VM. vnode is the vnode object returned by VM. Render() method.
At this point, we can see that when rendering for the first time, the logical call VM. \\\\\\\\\\\\\\\\\\

Vue.prototype.__patch__ = inBrowser ? patch : noop

The imported patch is in the src/platforms/web/runtime/patch.js file. The source code is as follows:

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'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

From the above source code, it can be seen that the path method finally defined is actually generated by calling createPatchFunction, while the nodeOps option passed in is a collection, an api collection for real dom operations, and the modules option is some operations on dom attributes during the patch, such as class, style, etc., so VM. \\\\\\\\\\.
The createPatchFunction method is defined in the src/core/vdom/patch.js file (because the source code is too long, not all of them will be displayed). Finally, a patch method is returned. The source code is as follows:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // Delete logical subcomponent destroy
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = [] // Call insert hook to use

    // Logical rendering of subcomponents
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // Determine whether it is a real dom
      const isRealElement = isDef(oldVnode.nodeType) 
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        // First render to real dom
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          // Server rendering related
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          // Server rendering related
          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.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode) // Convert real dom to vnode object
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        // Mount vnode to real dom
        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
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

As mentioned before, the first rendering call in VM. Update is VM. El = VM. Patch (VM. El, vnode, drafting, false / * removeonly * /), which is actually to call the path method of the above source code. The first parameter passed in has a value and is the real DOM, so if (isUndef(oldVnode)) logic and else logic are not used. The isRealElement defined in it is true, so if (! Isrea) logic is not used Llelement & & samevnode (oldvnode, vnode)) logic, else logic, if (isRealElement) logic (service side rendering related logic omitted), call emptyNodeAt method to convert real DOM into vnode object, then get oldElm and parentElm dom nodes, and call createElm method. This method is to mount vnode into real dom. The source code is as follows:

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
    // Attempt to create component node
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    // Insertion order: first child, then parent, last parent is mounted on the document dom
    if (isDef(tag)) { // Element node
      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
          )
        }
      }

      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        // Determine whether the child node recursively calls createElm and inserts it into the parent dom
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // Parent mount node current vnode node reference node
        // The parentElm is undefined when the component is in use
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) { // Comment Nodes
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else { // Text node
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
}

The main logic of the above source code is to first call the createComponent method to try to create a component node. Here we are the normal node, so we return it as undefined. Then we get the data, children and tag from the incoming VNode object. The corresponding data are {attrs: {id: "app"}}}, [VNode] ('Hello Vue! 'text VNode object) and div tag. After that, we will judge the tag and pass the nodeOps api creates real dom, and finally calls insert to insert the parent node in html, that is, the incoming parentElm parameter is the parent node in html.

After the createElm method is run, the following code will be run at the end of the patch method:

if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0)
}

Why do you need to execute this method for the last time? Because the newly created dom node is inserted in the createElm method, and the old node in html, that is, oldVnode, still exists. Therefore, you need to call this method to determine whether to delete the old node.

Now the VM. Update process is recorded.

Posted by Imad on Thu, 05 Dec 2019 03:34:00 -0800