Runtime Only & Runtime + Compiler

When initializing our vue.js project via vuE-CLI, we were asked to use Runtime Only or Runtime + Compiler.

  • Runtime Only

With this version, the.vue file is usually precompiled into Javascript with webPack’s vue-loader tool, and since it is done at compile time, it contains only the run-time vue.js code, which is also lighter in size.

  • Runtime + Compiler

If we do not precompile the code, but use vue’s template property and pass in a string, we need to compile the template on the client side. As follows:

Because in Vue 2.0, the final rendering is done by the render function, if you write the template property, you need to compile to the render function, and this compilation happens at runtime, so you need a version with the compiler.

Compilation takes a toll on performance, so Runtime Only vue.js is generally preferred.

Data driven

The core idea of Vue. js is data-driven. By data-driven, we mean that the view is generated by data, and we modify the view not by manipulating the DOM directly, but by modifying the data. This greatly simplifies the amount of code compared to the traditional front-end development of dom manipulation. Especially when the interaction is complex, focusing only on data changes makes the logic of the code very clear. Because the DOM becomes a mapping of the data, all of our logic is to modify the data without touching the DOM, which makes the code very maintainable.

Vue’s overall execution process

How are the templates and data rendered into the final DOM?

new Vue()

  1. New Vue() instantiates a Vue, so Vue is a constructor. When instantiated, Vue will be executed. New Vue() is instantiated as options
// src/core/instance/index.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')}_init is defined on the Vue prototype. This is the current Vue instance. Calling this will execute vue.prototype. _init
  this._init(options)
}
Copy the code

init

  1. Call this._init(options). The _init method is actually defined on vue.prototype, which takes an options argument, while vue.prototype. _init is defined on initMixin()
// src/core/instance/init.js

Vue.prototype._init = function (options? :Object) {
	// VM represents the current component instance
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // Merge the options passed by the user with the options property of the current constructor and the options property of its parent constructor. And mount the $options attribute to the Vue instance
      // The mergeOptions function merges the parent and child objects into a new object based on some merging strategy. First, recursively merge extends and mixins onto parent
      // Then create an empty object, options, and iterate over the parent, merging each item of the parent into the empty object, options, by calling mergeField.
      // Then iterate over the child and merge the attributes that exist in the child but are not in the parent into the empty object options by calling mergeField.
      // Finally, options is the result of the final merge, which is returned.
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if(process.env.NODE_ENV ! = ='production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // Initialize the lifecycle, mainly by mounting some properties on the Vue instance and setting default values
    initLifecycle(vm)
    // Initialize the event
    initEvents(vm)
    // Initialize render
    initRender(vm)
    // Triggers the beforeCreate lifecycle hook function
    callHook(vm, 'beforeCreate')
    // Initialize the injections
    initInjections(vm) 
    / / initialize the props, the methods, data, computed, watch
    initState(vm) 
    // initialize provide
    initProvide(vm) 
    callHook(vm, 'created')

    /* istanbul ignore if */
    if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
}
Copy the code

As you can see, the _init() method executes the following methods in sequence: InitLifecycle (VM), initEvents(VM), initRender(VM), callHook(VM, ‘beforeCreate’), initremedy (VM), initState(VM), initProvide(VM), callHook(VM, ‘created’)

When the initState() method is executed, initProps, initMethods, initData/observe, initComputed, and initWatch are executed respectively. Initialize props, Methods, data, computed, and Watch.

After the _init() method completes a series of initialization operations, determine whether the current vm.$options has an EL attribute on it. If so, call vm.$mount(vm.$options.el) to mount the VM, with the goal of rendering the template into the final DOM. Normally when new Vue() is passed in the el attribute.

$mount

  1. If the source code for the with-compiler version of vue, which is the template file we wrote, is compiled into the render function via entry-runtime-with-compiler, $mount runtime/index

  2. For Runtime Only versions of vue without the with-compiler, the Webpack and Loader handle compiling the template into the render function, Then execute the $mount method in Runtime /index.

So the $mount method in entry-Runtime-with-compiler actually does webPack and Loader stuff, but at compile time, which is the compile stage.

Let’s look at the $mount process in the with-Compiler version of the Vue source code.

// src/platforms/web/entry-runtime-with-compiler.js

// Get the $mount method from the vue prototype, assign it to the mount variable, and save it
const mount = Vue.prototype.$mount
// Then overwrite the vue prototype's $mount method, which contains the compilation method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && query(el)

  /* istanbul ignore if */
  // Vue cannot be mounted on root nodes such as body and HTML
  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
  // resolve template/el and convert to render function
  // If there is no render method in options, compile template and convert to render function
  if(! options.render) {// Get the template defined in options
    let template = options.template
    /* if template exists, outerHTML*/ for el does not exist
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          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) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
        mark('compile')}/* Compile template into render function, which returns render and staticRenderFns. This is a compile-time optimization for vue, static does not require patch during VNode update, optimizes performance */
      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

      /* istanbul ignore if */
      if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
        mark('compile end')
        measure(`vue The ${this._name} compile`.'compile'.'compile end')}}}// Const mount = Vue. Prototype.$mount */ const mount = Vue
  return mount.call(this, el, hydrating)
}
Copy the code

As can be seen:

  1. First check if options has the Render method
  • Without the Render method,
    1. Check if there is a template attribute in options,
      • If options.template is a string, template = idToTemplate(template)
      const idToTemplate = cached(id= > {
        const el = query(id)
        return el && el.innerHTML
      });
      Copy the code
    • If options.template is a Node, template = template.innerhtml
    • If no options. Template, template = getOuterHTML(el)
    1. Once you get the template property, fetch the render method via compileToFunctions(), so pass our template to compileToFunctions(), CompileToFunctions () this method will run a bunch of compileToFunctions on the template we wrote to generate the render function. All the information in the template is contained by the render function, the attributes in the data used in the template are turned into JS variables, and the V-model, V-for, V-ON and so on in the template are turned into JS logic. This process is the compilation process.
    2. Assign the render function to options.render
    3. Then mount. Call ()
  • If options has the render method, just call mount.call().

Call (this, el, hydrating), which is the $mount on the Vue prototype saved above.

Vue. Prototype.$mount () {mount ();

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

Vue.prototype.$mount = function (el, hydrating) {
  return mountComponent(
    this,
    el && query(el, this.$document),
    hydrating
  )
};
Copy the code

The mountComponent() method is implemented as follows:

// src/core/instance/lifecycle.js

export function mountComponent (vm: Component, el: ? Element, hydrating? : boolean) :Component {
  vm.$el = el
  if(! vm.$options.render) {/* Create an empty VNode when the render function does not exist */
    vm.$options.render = createEmptyVNode
    if(process.env.NODE_ENV ! = ='production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0)! = =The '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  /* Triggers beforeMount hook */
  callHook(vm, 'beforeMount')

  // Define an updateComponent variable
  let updateComponent
  /* istanbul ignore if */
  if(process.env.NODE_ENV ! = ='production' && config.performance && mark) {
    updateComponent = () = > {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    // Assign a value to the updateComponent variable
    updateComponent = () = > {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  /* Create an instance of Watcher, and pass updateComponent as the second argument to the Watcher object
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    /* flag bit indicating that the component has been mounted */
    vm._isMounted = true
    /* Triggers a mounted hook
    callHook(vm, 'mounted')}return vm
}
Copy the code

In the mountComponent() method:

    1. Check whether vm.$options.render exists. If not, assign createEmptyVNode to Options. This method creates an annotated VNode
    1. The callHook function is then called to trigger the beforeMount life cycle hook function, which is triggered here. Here you can understand the life cycle and determine if the template format is correct (2.1 does this), and you can also understand that the render function does not form a virtual DOM and does not actually render the page content
    1. UpdateComponent = () => {vm._update(vm._render(), hydrating)}
    1. Render Watcher is an instance of Watcher. The updateComponent function is passed to the Watcher class as a second argument, which means that all data read in the updateComponent function will be monitored by Watcher.

Let’s look at the Watcher class definition:

// src/core/observer/watcher.js

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object, isRenderWatcher? : boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !! options.deepthis.user = !! options.userthis.lazy = !! options.lazythis.sync = !! options.syncthis.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set(a)this.newDepIds = new Set(a)this.expression = process.env.NODE_ENV ! = ='production'
      ? expOrFn.toString()
      : ' '
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop process.env.NODE_ENV ! = ='production' && warn(
          `Failed watching path: "${expOrFn}"` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /** * Evaluate the getter, and re-collect dependencies. */
  get () {
    // Put the current Watcher instance on dep. target, a global object that points to the current Watcher instance
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "The ${this.expression}"`)}else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /** * Add a dependency to this directive. */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)}}}/** * Clean up for dependency collection. */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)}}let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /** * Subscriber interface. * Will be called when a dependency changes. */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)}}/** * Scheduler job interface. * Will be called by the scheduler. */
  run () {
    if (this.active) {
      const value = this.get()
      if( value ! = =this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "The ${this.expression}"`)}}else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /** * Depend on all deps collected by this watcher. */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /** * Remove self from all dependencies' subscriber list. */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)}let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)}this.active = false}}}Copy the code

As you can see, the constructor of the Watcher class takes four arguments:

  • Vm: Component, —- VM indicates the current Component instance
  • ExpOrFn: string | Function – expression/Function?
  • Cb: Function, —- This is the callback Function
  • options? 😕 Object, —- This is an optional parameter, Object type
  • isRenderWatcher? : Boolean —- This is a Boolean value indicating whether it is a render watcher

Then use the argument passed to new Watcher() above:

new Watcher(vm, updateComponent, noop, {
  before () {
    if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
Copy the code

The updateComponent() method is passed to the expOrFn parameter, options is an object containing the before method, isRenderWatcher is true.

Constructor of the Watcher class automatically.

  1. this.lazy = !! Options. Lazy: this. Lazy is false because options do not define the lazy attribute.
  2. ExpOrFn is a function, so this.getter = expOrFn;
  3. At the end of this. Value = this.lazy? Use undefined: this.get() in this expression, the this.get() method is executed, assigning the return value to this.value.
  4. In this.get(), pushTarget(this) puts the current Watcher instance on dep.target, value = this.gett. call(vm, vm), and executes this.getter. UpdateComponent () calls _render(), which is generated from the template. To render the template, you need to access the ATTRIBUTES of the VM and trigger the getters for the data used. This is a dependency collection process. The Watcher instance will hold a closure Dep for all the data required for rendering. The vm._update() method is called in updateComponent(), which in turn calls vm._render().
updateComponent = () = > {
  vm._update(vm._render(), hydrating)
}
Copy the code

Depending on the lifO nature of the stack, vm._render() is executed first, followed by vm._update(). The vm._render method generates a virtual Node and then calls vm._update to update the DOM.

Next, let’s look at the vm._render() method.

render

Vue’s _render method is a private method of the instance that renders it as a virtual Node.

Calling vm._render() method actually executes _render() method on Vue prototype. In the process of executing render method, the corresponding getter method will be triggered for attributes in data accessed to collect dependencies and generate VNodes.

// src/core/instance/render.js

Vue.prototype._render = function () :VNode {
  const vm: Component = this
  // Get the render method on $options and assign it to the render variable by destructing it
  const { render, _parentVnode } = vm.$options

  if (_parentVnode) {
    vm.$scopedSlots = normalizeScopedSlots(
      _parentVnode.data.scopedSlots,
      vm.$slots,
      vm.$scopedSlots
    )
  }

  // set parent vnode. this allows render functions to have access
  // to the data on the placeholder node.
  vm.$vnode = _parentVnode
  
  // render self
  let vnode
  try {
    // There's no need to maintain a stack because all render fns are called
    // separately from one another. Nested component's render fns are called
    // when parent component is patched.
    currentRenderingInstance = vm
    /* Call the render function to return a VNode */
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if(process.env.NODE_ENV ! = ='production' && vm.$options.renderError) {
      try {
        vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
      } catch (e) {
        handleError(e, vm, `renderError`)
        vnode = vm._vnode
      }
    } else {
      vnode = vm._vnode
    }
  } finally {
    currentRenderingInstance = null
  }
  // if the returned array contains only a single node, allow it
  if (Array.isArray(vnode) && vnode.length === 1) {
    vnode = vnode[0]}// return empty vnode in case the render function errored out
  if(! (vnodeinstanceof VNode)) {
    if(process.env.NODE_ENV ! = ='production' && Array.isArray(vnode)) {
      warn(
        'Multiple root nodes returned from render function. Render function ' +
        'should return a single root node.',
        vm
      )
    }
    vnode = createEmptyVNode()
  }
  // set parent
  vnode.parent = _parentVnode
  return vnode
}
Copy the code

In the _render() method above, we can see the core code is:

// Assign the current vue instance to currentRenderingInstance
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code

Execute render() to generate a VNode, and you can see that the second argument to render. Call () is vm.$createElement, passed to render().

The first argument to the Render function is the createElement method

So the render function actually executes vm.$createElement.

Vm.$createElement is defined when the initRender() method executes:

/* Bind the createElement function to the instance. The VM is stored in a closure and cannot be modified. The vm instance is fixed. This way we can get the correct context rendering */
// Args order: tag, data, children, normalizationType, alwaysNormalize
// alwaysNormalize is false if the template method is used to initialize render, and true if the render function is usedThis is done in the subsequent _createElement'children'Parameter processing is exquisite// used by the template-compiled render function
vm._c = (a, b, c, d) = > createElement(vm, a, b, c, d, false)

// The user handwrites the render method
vm.$createElement = (a, b, c, d) = > createElement(vm, a, b, c, d, true)
Copy the code

$createElement = vm. createElement = vm. createElement = vm. createElement = vm. createElement = vm. createElement = vm. createElement = vm. createElement; And the createElement method is called internally.

// src/core/vdom/create-element.js

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

The core of the createElement() method is to execute _createElement(context, tag, data, children, normalizationType) and return the result. So the actual method to create a VNode is _createElement.

// src/core/vdom/create-element.js

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  /* If data is not defined (undefined or null) or data's __ob__ is defined (that is, the Oberver object is bound to it), then create an empty node */
  if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
      `Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render! ',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
   /* Create an empty node */ if the tag does not exist
  if(! tag) {// in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.key) && ! isPrimitive(data.key) ) {if(! __WEEX__ || ! ('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0= = ='function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // Normalize children into an array
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      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
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      /* Find the tag in the option components of the VM instance, if it exists, it is a component, create the corresponding node, and Ctor is the component constructor */
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      /* Unknown element, checked at run time, because the parent component may allocate a namespace when sequence the child component */
      vnode = new VNode(
        tag, data, children,
        undefined.undefined, context
      )
    }
  } else {
    // direct component options / constructor
    /* Tag is the component's constructor when it is not a string
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
Copy the code

The _createElement method takes five arguments:

  • Context represents the context of a VNode. It is of type Component.
  • Tag represents a tag, which can be either a string or a Component.
  • Data refers to VNode data. It is a VNodeData type. You can find its definition in flow/vnode.js.
  • Children represents the children of the current VNode, which can be of any type and then needs to be normalized as a standard VNode array;
  • NormalizationType represents the type of child node specification, and the method varies depending on the type specification, depending on whether the Render function is compiled or handwritten.

Standardization of children

simpleNormalizeChildren
  1. The simpleNormalizeChildren method call scenario is that the Render function is compiled. The functional Component returns an array instead of a root node. So the array.prototype. concat method is used to flatten the entire children Array so that it is only one layer deep.
// src/core/vdom/helpers/normalzie-children.js

export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
Copy the code
normalizeChildren
  1. The normalizeChildren method can be called in two scenarios. One scenario is that the render function is written by the user, and when children have only one node, Vue.js allows the user to write children as a basic type to create a single simple text node at the interface level. In this case, createTextVNode is called to create a VNode of the text node. Another scenario is when a nested array is generated when slot, V-for is compiled, and the normalizeArrayChildren method is called
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}
Copy the code
function normalizeArrayChildren (children: any, nestedIndex? : string) :Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
    // nested
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
        // merge adjacent text nodes
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        // merge adjacent text nodes
        // this is necessary for SSR hydration because text nodes are
        // essentially merged when rendered to HTML strings
        res[lastIndex] = createTextVNode(last.text + c)
      } else if(c ! = =' ') {
        // convert primitive to vnode
        res.push(createTextVNode(c))
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        // merge adjacent text nodes
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // default key for nested array children (likely generated by v-for)
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__ `
        }
        res.push(c)
      }
    }
  }
  return res
}
Copy the code

NormalizeArrayChildren takes two arguments, children representing the child node to be normalized and nestedIndex representing the nestedIndex, since a single child may be an array type. NormalizeArrayChildren the main logic of normalizeArrayChildren is to traverse children, get a single node C, and determine the type of C. If it is an array type, recursively call normalizeArrayChildren. If it is a base type, the createTextVNode method is used to convert it to VNode. Otherwise it is already a VNode. If children is a list and the list is nested, update its key according to nestedIndex. One thing to note here is that during the traversal, the following is done for all three cases: if there are two consecutive text nodes, they are merged into a single text node.

After normalization of children, children becomes an Array of type VNode.

Continuing with the _createElement method:

  1. CreateComponent Creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. CreateComponent creates a component VNode. Otherwise, create a VNode with an unknown label.
  2. If it tags a Component type, call createComponent directly to create a VNode of Component type. CreateComponent essentially returns a VNode.

Here’s a look at creating a component VNode using createComponent:

// src/core/vdom/create-component.js

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
  // Construct the subclass constructor
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeofCtor ! = ='function') {
    if(process.env.NODE_ENV ! = ='production') {
      warn(`Invalid Component definition: The ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  // Install the component hook function
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  // Instantiate VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
    data, undefined.undefined.undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  return vnode
}
Copy the code

The core flow of createComponent is:

  1. Construct the subclass constructor
  2. Install component hook functions
  3. Instantiate a vNode that, unlike a vNode of a normal element, does not have children.

VNode

Virtual DOM uses a native JS object to describe a DOM node, so it is much cheaper than creating a DOM. In vue.js, the Virtual DOM is described by the VNode Class:

export default class VNode {
  constructor (tag? : string, data? : VNodeData, children? :?Array<VNode>, text? : string, elm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? :Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
Copy the code

VNode is an abstract description of the real DOM. Its core definition is nothing more than a few key attributes, tag name, data, child nodes, key values, etc. Other attributes are used to extend the flexibility of VNode and implement some special features. Since VNode is only used for rendering that maps to the real DOM and does not need to include methods to manipulate the DOM, it is lighter and simpler than real DOM nodes.

Remember that the updateComponent() method was actually vm._update(vm._render(), hydrating)?

After the vm._render() method is executed, we take vNode and pass it to vm._update() to execute the _update() method, which is called at two times, one for first render and one for data update. The _update method is used to render a VNode into a real DOM.

_update of vue is a private method of the instance, the actual _update() method is defined on the vue prototype, and vue.prototype._update is defined in lifecycleMixin() :

// src/core/instance/lifecycle.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
    // based on the rendering backend used.
    if(! prevVnode) {// initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
      // updates
      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.
  }
Copy the code

The vm._patch method is called in the update method

patch

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

// In the browser, __patch__ on the Vue prototype is patch
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code
// src/platforms/web/runtime/patch.js

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

The actual patch is the return value from calling the createPatchFunction() method. An object is passed in with the nodeOps parameter and modules parameter. NodeOps encapsulates a set of DOM manipulation methods, and Modules defines implementations of module hook functions.

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

  const { modules, nodeOps } = backend

  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]])
      }
    }
  }

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

  function createRmCb (childElm, listeners) {
    function remove () {
      if (--remove.listeners === 0) {
        removeNode(childElm)
      }
    }
    remove.listeners = listeners
    return remove
  }

  function removeNode (el) {
    const parent = nodeOps.parentNode(el)
    // element may have already been removed due to v-html / v-text
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el)
    }
  }

  function isUnknownElement (vnode, inVPre) {
    return(! inVPre && ! vnode.ns && ! ( config.ignoredElements.length && config.ignoredElements.some(ignore= > {
          return isRegExp(ignore)
            ? ignore.test(vnode.tag)
            : ignore === vnode.tag
        })
      ) &&
      config.isUnknownElement(vnode.tag)
    )
  }

  let creatingElmInVPre = 0

  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
    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
          )
        }
      }

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

      /* istanbul ignore if */
      if (__WEEX__) {
        ...
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

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

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      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)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true}}}function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
      vnode.data.pendingInsert = null
    }
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode)
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode)
    }
  }

  function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i
    // hack for #4339: a reactivated component with inner transition
    // does not trigger because the inner node's created hooks are not called
    // again. It's not ideal to involve module-specific logic in here but
    // there doesn't seem to be a better way to do it.
    let innerNode = vnode
    while (innerNode.componentInstance) {
      innerNode = innerNode.componentInstance._vnode
      if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
        for (i = 0; i < cbs.activate.length; ++i) {
          cbs.activate[i](emptyNode, innerNode)
        }
        insertedVnodeQueue.push(innerNode)
        break}}// unlike a newly created component,
    // a reactivated keep-alive component doesn't insert itself
    insert(parentElm, vnode.elm, refElm)
  }

  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)
      }
    }
  }

  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) {
        createElm(children[i], insertedVnodeQueue, vnode.elm, null.true, children, i)
      }
    } else if (isPrimitive(vnode.text)) {
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }

  function isPatchable (vnode) {
    while (vnode.componentInstance) {
      vnode = vnode.componentInstance._vnode
    }
    return isDef(vnode.tag)
  }

  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }

  // set scope id attribute for scoped CSS.
  // this is implemented as a special case to avoid the overhead
  // of going through the normal attribute patching process.
  function setScope (vnode) {
    let i
    if (isDef(i = vnode.fnScopeId)) {
      nodeOps.setStyleScope(vnode.elm, i)
    } else {
      let ancestor = vnode
      while (ancestor) {
        if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {
          nodeOps.setStyleScope(vnode.elm, i)
        }
        ancestor = ancestor.parent
      }
    }
    // for slot content they should also get the scopeId from the host instance.
    if(isDef(i = activeInstance) && i ! == vnode.context && i ! == vnode.fnContext && isDef(i = i.$options._scopeId) ) { nodeOps.setStyleScope(vnode.elm, i) } }function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    }
  }

  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])
      }
    }
  }

  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)
        }
      }
    }
  }

  function removeAndInvokeRemoveHook (vnode, rm) {
    if (isDef(rm) || isDef(vnode.data)) {
      let i
      const listeners = cbs.remove.length + 1
      if (isDef(rm)) {
        // we have a recursively passed down rm callback
        // increase the listeners count
        rm.listeners += listeners
      } else {
        // directly removing
        rm = createRmCb(vnode.elm, listeners)
      }
      // recursively invoke hooks on child component root node
      if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
        removeAndInvokeRemoveHook(i, rm)
      }
      for (i = 0; i < cbs.remove.length; ++i) {
        cbs.remove[i](vnode, rm)
      }
      if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
        i(vnode, rm)
      } else {
        rm()
      }
    } else {
      removeNode(vnode.elm)
    }
  }

  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 in correct relative positions
    // during leaving transitions
    constcanMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
      checkDuplicateKeys(newCh)
    }

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

  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}}}}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
    }
  }

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
    if (oldVnode === vnode) {
      return
    }

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

    const elm = vnode.elm = oldVnode.elm

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

    // 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))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      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)) {
      if (isDef(oldCh) && isDef(ch)) {
        if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if (isDef(ch)) {
        if(process.env.NODE_ENV ! = ='production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ' ')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, ' ')}}else if(oldVnode.text ! == vnode.text) { nodeOps.setTextContent(elm, vnode.text) }if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

  function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue
    } else {
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
      }
    }
  }

  let hydrationBailed = false
  // list of modules that can skip create hook during hydration because they
  // are already rendered on the client or has no need for initialization
  // Note: style is excluded because it relies on initial clone for future
  // deep updates (#7063).
  const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')

  // Note: this is a browser-only function so we can assume elms are DOM nodes.
  function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
    let i
    const { tag, data, children } = vnode
    inVPre = inVPre || (data && data.pre)
    vnode.elm = elm

    if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
      vnode.isAsyncPlaceholder = true
      return true
    }
    // assert node match
    if(process.env.NODE_ENV ! = ='production') {
      if(! assertNodeMatch(elm, vnode, inVPre)) {return false}}if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
      if (isDef(i = vnode.componentInstance)) {
        // child component. it should have hydrated its own tree.
        initComponent(vnode, insertedVnodeQueue)
        return true}}if (isDef(tag)) {
      if (isDef(children)) {
        // empty element, allow client to pick up and populate children
        if(! elm.hasChildNodes()) { createChildren(vnode, children, insertedVnodeQueue) }else {
          // v-html and domProps: innerHTML
          if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {
            if(i ! == elm.innerHTML) {/* istanbul ignore if */
              if(process.env.NODE_ENV ! = ='production' &&
                typeof console! = ='undefined' &&
                !hydrationBailed
              ) {
                hydrationBailed = true
                console.warn('Parent: ', elm)
                console.warn('server innerHTML: ', i)
                console.warn('client innerHTML: ', elm.innerHTML)
              }
              return false}}else {
            // iterate and compare children lists
            let childrenMatch = true
            let childNode = elm.firstChild
            for (let i = 0; i < children.length; i++) {
              if(! childNode || ! hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) { childrenMatch =false
                break
              }
              childNode = childNode.nextSibling
            }
            // if childNode is not null, it means the actual childNodes list is
            // longer than the virtual children list.
            if(! childrenMatch || childNode) {/* istanbul ignore if */
              if(process.env.NODE_ENV ! = ='production' &&
                typeof console! = ='undefined' &&
                !hydrationBailed
              ) {
                hydrationBailed = true
                console.warn('Parent: ', elm)
                console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children)
              }
              return false}}}}if (isDef(data)) {
        let fullInvoke = false
        for (const key in data) {
          if(! isRenderedModule(key)) { fullInvoke =true
            invokeCreateHooks(vnode, insertedVnodeQueue)
            break}}if(! fullInvoke && data['class']) {
          // ensure collecting deps for deep class bindings for future updates
          traverse(data['class'])}}}else if(elm.data ! == vnode.text) { elm.data = vnode.text }return true
  }

  function assertNodeMatch (node, vnode, inVPre) {
    if (isDef(vnode.tag)) {
      return vnode.tag.indexOf('vue-component') = = =0| | (! isUnknownElement(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()) ) }else {
      return node.nodeType === (vnode.isComment ? 8 : 3)}}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)
      if(! isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null.null, removeOnly)
      } else {
        if (isRealElement) {
          // 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.')}}// either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

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

        // create new node
        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([oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

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

CreatePatchFunction internally defines a series of helper methods that eventually return a patch method that is assigned to the vm.patch called in the vm._update function.

The patch method itself receives four parameters:

  • OldVnode represents the oldVnode, which can either be nonexistent or a DOM object;
  • Vnode represents the vnode returned by _render;
  • Hydrating indicates whether it is server side rendering.
  • RemoveOnly is for transition-group

For the first rendering, when executing patch, the vm.$el is passed to the DOM object whose id is app. Vm.$el was assigned to the mountComponent function earlier. Vnode corresponds to the return value of the render function, hydrating false for non-server rendering, and removeOnly false.

With these input parameters identified, we go back to the execution of the Patch function and look at the key steps: The oldVnode we pass in is actually a DOM Container, so isRealElement is true. Then we convert the oldVnode into a VNode object using emptyNodeAt. Then call the createElm method. CreateElm creates a real DOM from a virtual node and inserts it into its parent node.

CreateElm creates a real DOM from a virtual node and inserts it into its parent node.

During createElm, if the vNode node does not contain a tag, it may be an annotation or plain text node that can be inserted directly into the parent element.

DOM

Related articles:

  • Depend on the collection
  • Distributed update