When we mount through an instance of new Vue(), we replace the corresponding mounted DOM node. Now we through the source point of view analysis of its implementation behind the main process.

Vue constructor

Let’s look at a simple example of Vue:

<div id="app">{{ msg }}</div>

<script>
  new Vue({
    el: '#app',
    data: {
      msg: 'hello vue'}});</script>
Copy the code

Now let’s look at the definition of the Vue constructor. It defined in SRC/core/instance/index in js:

function Vue (options) {
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}

// Define various methods on vue. prototype
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
Copy the code

See that Vue is a constructor that receives an Option configuration object. The following mixin methods define methods on top of Vue prototypes. For example, _init() is defined in initMixin when new Vue() is called. Let’s look at the implementation of _init().

vm._init()

Vm. _init () method defined in SRC/core/instance/init. Js, we delete the performance monitoring of relevant code:

let uid = 0

Vue.prototype._init = function (options? : Object) {
  const vm: Component = this
  vm._uid = uid++

  / /... Performance monitoring correlation

  // The tag of the VM instance
  vm._isVue = true
 
  if (options && options._isComponent) {
    // Configuration merge of component instances
    initInternalComponent(vm, options)
  } else {
    / / general configuration of the Vue instance merger, mainly put some component of the global directive, merge the filter to the vm
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  
  vm._self = vm
  initLifecycle(vm)  // Maintain vm. parent and vm. children
  initEvents(vm)  
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)  // Handle status monitoring
  initProvide(vm)
  callHook(vm, 'created')

  // Use new Vue to mount components without going there
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
Copy the code

The initInternalComponent() method handles the configuration of the component instance. The mergeOptions method is used in this example. . It is mainly the Vue option on some of the global component, directive, filter merge to vm, merge strategy within the code we again after detailed analysis.

After that, different module initializations of various instantiations, such as the initState() method, handle state-related code, such as response handling of data data, etc. The callHook() method executes the lifecycle hook, and initState() is called after the beforeCreate hook, which is why we can only get the VM state from the CREATE hook.

Finally, if the EL is provided in the new Vue() configuration object, we call the vm.$mount() method to mount it. Let’s examine the code for the $mount() method.

vm.$mount()

The way instances are mounted is platform-specific, and the compile and runtime entry is different. The entry point for the compiled version is to handle the configuration object Template on a run-time basis, turning the template string into the Render () method. So we start with the compile version of the mount entry, which is defined in SRC /platforms/web/entry-runtime-with-compiler.js:

// Mount the runtime version of the cache
const mount = Vue.prototype.$mount

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  // Return the DOM object of el
  el = el && query(el)

  // Cannot be mounted in body or document
  if (el === document.body || el === document.documentElement) { process.env.NODE_ENV ! = ='production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // If options does not have a render method, get render according to template or el
  if(! options.render) {let template = options.template
    if (template) {
      // Process template as an HTML string
      if (typeof template === 'string') {
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
          if(process.env.NODE_ENV ! = ='production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`.this)}}}else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if(process.env.NODE_ENV ! = ='production') {
          warn('invalid template option:' + template, this)}return this}}else if (el) {
      // use the outhTML of the DOM corresponding to el as template
      template = getOuterHTML(el)
    }
    
    if (template) {

      // Compile template to get render
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // Call the Runtime mount method
  return mount.call(this, el, hydrating)
}
Copy the code

We start by running the version’s mount function with a variable cache, and then define the compiled version’s $mount() method. Method handles the template to process the configuration object and, if provided, the various configurations and finally the HTML string. This example doesn’t have a template, so get the OuterHTML of the DOM corresponding to el as the template.

Once we get the template we call compileToFunctions() to compile the template into the render function. The template is handled in vue using the virtual DOM, and the render() method is used to obtain the virtual DOM node of the corresponding template. Finally, call the mount method of the run version.

Running version of the mount method defined in SRC/platforms/web/runtime/index in js:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
Copy the code

The method is simple: turn the EL into a DOM object and call the mountComponent() method. The methods defined in SRC/core/instance/lifecycle. In js:

export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  vm.$el = el

  if(! vm.$options.render) { vm.$options.render = createEmptyVNodeif(process.env.NODE_ENV ! = ='production') {
      / /.. An error is reported if template exists}}Call the beforeMount hook function
  callHook(vm, 'beforeMount')

  let updateComponent
  if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
    / /.. Performance monitoring correlation
  } else {
    updateComponent = (a)= > {
      vm._update(vm._render(), hydrating)
    }
  }

  // Create a new render watcher that calls the get() method in the constructor
  // updateComponent will be executed
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true)
  hydrating = false

  $vnode represents the parent vnode, which is called only by the root instance
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')}return vm
}
Copy the code

The render method initially determines if the Render method is present, returns a method to create an empty node if not, and checks in the development environment to see if the version is running with the template configuration. We then define an updateComponent function as an argument to create a new Watcher instance, regardless of the implementation of Watcher, as long as we know that the updateComponent() method will be executed during Watcher instantiation. After executing this method, check whether the VM is the root instance. If so, call mounted.

vm._render()

After the updateComponent() method is executed, the vm._render() method first returns the virtual node corresponding to the instance. A virtual node is a simple REPRESENTATION of a DOM node using an ordinary JS object. Vm. And _render () method defined in SRC/core/instance/render in js:

Vue.prototype._render = function () :VNode {
  const vm: Component = this
  const { render, _parentVnode } = vm.$options

  // Save the parent virtual node
  vm.$vnode = _parentVnode
 
  let vnode
  try {
    currentRenderingInstance = vm
    // Call the render function on the VM
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    / /... Error correlation handling
  } finally {
    currentRenderingInstance = null
  }

  // ...
  
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
Copy the code

This method calls the render() method on options with vm.$createElement as an argument. Vm.$createElement is defined as:

vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)
Copy the code

Obviously, this method is a wrapper around the createElement() method, which creates a virtual node. So we can use this argument ourselves when we write the render() function, for example:

new Vue({
  el: '#app'.data: {
    msg: 'hello vue'
  },
  render(h) {
    return h('div', {}, this.msg)
  }
});
Copy the code

The h parameter is vm.$createElement. We won’t use this parameter in our example, because we didn’t write the render function by hand, but used vue compiled render. This time we call vm._c:

vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)
Copy the code

The difference between the two is the last parameter. Let’s look at the definition of the createElement() function. Define in SRC /core/vdom/create-element.js:

export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
  // Data is not transmitted
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    // Format children
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
Copy the code

This method first deals with cases where the data parameter is not a property configuration object. Then call the _createElement() method, which is defined in the same file. This method handles the children node first, and in the case of a hand-written render function, converts children to an array of vNode objects:

if (Array.isArray(children) &&
    typeof children[0= = ='function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
}
Copy the code

The instance is a virtual node created by calling vm._c, so the simpleNormalizeChildren() method is used, which flattens the array of children once, but only one layer.

Then, based on the tag judgment, call different methods to return the VNode. In this example, the tag is a div, which is a built-in tag for the platform.

if (config.isReservedTag(tag)) {
      // Platform reserved tags
      if(process.env.NODE_ENV ! = ='production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>. `,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
}
Copy the code

New VNode() returns a VNode object directly. The createComponent() method creates an implementation of the component node vNode, which we’ll examine later. At this point. Vnode = vnode; vnode = vnode; vnode = vnode; vnode = vnode;

vm._update()

Call vm._update() with the virtual vNode returned after the vm._render() method. It defined in SRC/core/instance/lifecycle. In js:

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
  // Vue.prototype.__patch__ is injected in entry points

  if(! prevVnode) {// Mount it for the first time
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()

  if (prevEl) {
    prevEl.__vue__ = null
  }
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }

  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
}
Copy the code

This method updates the DOM by calling the vm.__patch__() method to compare the old and new virtual nodes and find the difference caused by the state change. Return the updated DOM node and assign it to vm.$el. For the first mount:

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
Copy the code

The definition of vm.__patch__() is:

Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code

The definition of patch method is:

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

The Patch () method is returned by calling createPatchFunction(), where nodeOps is the wrapper around the DOM manipulation method, and Modules is the hook function that is called to create the VNode comparison procedure. Since VUE can be implemented cross-platform, the most important point of cross-platform is that the operation and parsing methods of Vnode are different, so a factory function is used to return the corresponding patch method.

Let’s look at the createPatchFunction() method. The createPatchFunction() method defines the utility functions used in the patch process. The code is quite long.

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

export function createPatchFunction(backend) { 
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend

  // Collect hook functions
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  / /... Encapsulation of patch process function

  / / returns the patch
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (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 = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType) // Whether it is a DOM node, the first mount is true
      if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
        // Diff on the same node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
      } else {
        if (isRealElement) {
          / / SSR
          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.')}}// Create a simple vnode
          oldVnode = emptyNodeAt(oldVnode)
        }

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

        // create new node
        // Create a DOM with vnode and insert it
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // destroy old node
        if (isDef(parentElm)) {
          // Delete the old node
          removeVnodes([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

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

Since our oldVnode was first mounted as vm.$el, the DOM object corresponding to el in the configuration, the isRealElement variable is true. So the following flow code is used:

if (isRealElement) {
  / /... SSR related
  // Create a simple vnode
  oldVnode = emptyNodeAt(oldVnode);
}

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

// create new node
// Create a DOM with vnode and insert it
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
);

// destroy old node
if (isDef(parentElm)) {
  // Delete the old node
  removeVnodes([oldVnode], 0.0);
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode);
}
Copy the code

The emptyNodeAt() method is used to convert the actual DOM into a virtual node.

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

This method stores the actual DOM in vnode.elm.

const parentElm = nodeOps.parentNode(oldElm);
Copy the code

Get the parent of the actual DOM, which in our case is the body node. Call createElm() to create a new DOM node for vNode and insert it into parentElm.

// Create a DOM with vnode and insert it
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
  if(isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode); } vnode.isRootInsert = ! nested;// Process of creating a vnode
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }

  const data = vnode.data;
  const children = vnode.children;
  const tag = vnode.tag;
  if (isDef(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 label node
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode);
    setScope(vnode);

    if (__WEEX__) {
      / /... Weex processing
    } else {
      // Recursively create the children dom node of vnode and insert it into vnode.elm
      createChildren(vnode, children, insertedVnodeQueue);
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      }
      // Insert the node created by vNode into the real DOM
      insert(parentElm, vnode.elm, refElm);
    }

    if(process.env.NODE_ENV ! = ='production'&& data && data.pre) { creatingElmInVPre--; }}else if (isTrue(vnode.isComment)) {
    // Create a comment node
    vnode.elm = nodeOps.createComment(vnode.text);
    insert(parentElm, vnode.elm, refElm);
  } else {
    // Create a text nodevnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); }}Copy the code

The createComponent() method is handled by the component vNode, where we return false to continue the logic. Nodeops.createelement () generates an empty DOM node and then calls createChildren() to insert the child node into the DOM created by the current VNode:

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if(process.env.NODE_ENV ! = ='production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
       // recursive call
      createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}
Copy the code

Call invokeCreateHooks() after insertion to execute the Create hook defined on moudles and vNode. This process mainly deals with the various properties defined by data on vNode, such as class, event, etc. They are in the implementation of the SRC/platforms/web/runtime/modules. Finally, we call the insert() method to insert the generated new DOM into the parent node, in this case the body. Let’s look at the definition of the insert() method:

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)
    }
  }
}
Copy the code

We’ll look at two inserted in nodeOps method, they are all defined in SRC/platforms/web/runtime/node – ops. In js:

export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
Copy the code

Obviously, this is all native DOM manipulation. So, after executing, view the DOM as

After inserting the DOM, remove the old nodes by calling removeVnodes() :

// Delete the old node
removeVnodes([oldVnode], 0.0)
Copy the code

Finally, the invokeInsertHook() method is called to execute the various hook functions after DOM insertion. At this point, the DOM corresponding to our new Vue() has been most successful in replacing and calling the original mounted node.

conclusion

New Vue() calls the _init() method of the instance for initialization, and then calls the $mount() method for mounting. Mount a new rendered Watcher and immediately execute the Getter for the Watcher. This is the updateComponent() function, which generates the virtual node with vm._render() and then calls vm._update() to patch the node. The patch process deals with oldVnode as a real DOM element. The process is probably to generate the DOM with the new VNode and then tune the original DOM instead.

It can be summarized as follows:

>>> Original address