Vue.js source code learning – data driven

One of the core ideas of Vue.js is data-driven. Data-driven means that views are generated by data drivers, and views are modified not by manipulating DOM directly, but by modifying data. Compared to traditional front-end development, using libraries such as jQuery to modify the DOM directly simplifies the amount of code. When interactions are complex, the logic of the code becomes very clear because the DOM becomes a mapping of the data, and all the logic is to modify the data without touching the DOM, making the code maintainable.

For example, the following uses template string syntax to render data into the DOM:

<div>{{ message }}</div>
Copy the code
var app = new Vue({
  el: '#app'.data: {
    message: 'Hello Vue! ',}})Copy the code

This will render Hello Vue! On the page. . The goal of this article is to figure out how templates and data are rendered into the final DOM.

What happened to New Vue

Let’s start by analyzing what happened to New Vue. Vue is actually a class, look at the source code, in the 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)
}
Copy the code

You can see from the code that the Vue can only be initialized with the new keyword, and then the this._init method is called. The method in the SRC/core/instance/init. Js is defined.

Vue initialization basically does a few things, merging configurations, initializing life cycles, initializing event center, initializing render, initializing Data, props, computed, Watcher, and so on.

Look at the code below, which is the end of the method.

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

At the end of initialization, if the el attribute is detected, the vm.$mount method is called to mount the VM, with the goal of rendering the template into the final DOM.

Two, Vue instance mount implementation

The VM is mounted in Vue through the $mount instance method, which is defined in multiple files because its implementation is platform – and build-dependent. The focus here is on the $mount implementation with the Compiler version.

SRC /platform/web/ entry-Runtime-with-compiler.js:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
) :Component {
  el = el && query(el)

  /* istanbul ignore if */
  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(! options.render) {let template = options.template
    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')}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')}}}return mount.call(this, el, hydrating)
}
Copy the code

This code first caches the $mount method on the prototype and then redefines it. The key logic in the code is that if the render method is not defined, the EL or template string is converted to the Render method. Finally, call the $mount method on the original prototype to mount it.

$mount on the original prototype method in SRC/platform/web/runtime/index defined js. Here’s what the $mount method does:

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

$mount takes two arguments. The first is the mounted element, which can be a string or a DOM object. If it is a string, the browser will call the Query method to convert it to a DOM object. The $mount method actually calls the mountComponent method. The methods defined in SRC/core/instance/lifecycle. The js file:

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') {
      /* 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
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  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 {
    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
  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) {
    vm._isMounted = true
    callHook(vm, 'mounted')}return vm
}
Copy the code

The core is to define the updateComponent method, call vm._render to create a VNode, and then call vm._update to update the DOM. You then instantiate a render Watcher, pass in the updateComponent method, and call that method.

Watcher’s two purposes here are to execute the callback function when it is initialized, and to execute the callback function when the data in the VM instance changes.

The vm._isMounted function sets vm. ismounted to true, indicating that the instance is mounted. Note that vm.$vnode represents the parent virtual Node of the Vue instance, so Null indicates that it is currently an instance of the root Vue.

Third, render

See below Vue and _render method, this method is the instance of a private method, are mainly used for the instance to render a VNode, defined in SRC/core/instance/render. The js file:

Vue.prototype._render = function () :VNode {
  const vm: Component = this
  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
    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

The most critical is the render method call, as follows:

vnode = render.call(vm._renderProxy, vm.$createElement)
Copy the code

$createElement (vm.$createElement); $createElement (vm.$createElement); $createElement (vm.$createElement); While vm.$createElement is used by the user’s handwritten Render method, internally the createElement method is called.

So vm._render finally returns a VNode by executing the createElement method. Let’s look at the implementation of createElement.

Fourth, the createElement method

The createElement method is used to create a VNode. It is defined in the SRC /core/vdom/create-element.js file:

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
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 createElement method is essentially a wrapper around the _createElement method, allowing more flexibility with the parameters passed in. After processing these parameters, the function _createElement that actually creates VNode is called:

export function _createElement(context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  // ...
  // Normalization of children
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  // Create a VNode
  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)))
    ) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(tag, data, children, undefined.undefined, context)
    }
  } else {
    // direct component options / constructor
    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 VNode context, which is of type Component. Tag represents a tag, which can be either a string or a Component. Data represents VNode data, which is a VNodeData type. Children represents the children of the current VNode, which is of any type and 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.

In fact, the _createElement method has two main processes, the normalization of children and the creation of vNodes.

4.1. Standardization of children

Each VNode may have several child nodes, which should also be of the VNode type. Because the fourth argument received by _createElement is of arbitrary type, children need to be normalized to a VNode type. Depending on the normalizationType, the normalizeChildren(children) and simpleNormalizeChildren(children) methods are called. Their definitions in the SRC/core/vdom/helpers/normalize – children. Js file:

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
}

export function normalizeChildren(children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
    ? normalizeArrayChildren(children)
    : undefined
}
Copy the code

SimpleNormalizationChildren method invocation scenario is render function is generated by the template compilation. Concat (array.prototype. concat) flattens the entire children Array to one level.

The normalizeChildren method can be called in two scenarios. One scenario is that the render function is written by the user, when children have only one node, and is of the basic type. In this case createTextVNode is called to create a VNode of a text node; NormalizeArrayChildren (); normalizeArrayChildren (); normalizeArrayChildren ();

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 parameters, children representing the child node to be normalized and nestedIndex representing the nestedIndex. 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 type.

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

4.2 Creating vNodes

_createElement creates a VNode. Create a VNode.

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)))
  ) {
    // component
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(tag, data, children, undefined.undefined, context)
  }
} else {
  // direct component options / constructor
  vnode = createComponent(tag, data, context, children)
}
Copy the code

CreateComponent Creates a component VNode by createComponent, createComponent creates a component VNode by createComponent, createComponent creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. CreateComponent creates a component VNode by createComponent. Otherwise, create a VNode with an unknown label. If the tag is a Component type, call createComponent directly to create a VNode of the Component type.

Vm. _render creates a VNode and renders it as a DOM. This is done using vm._update

Fifth, the update

Vue’s _update method is a private method of the instance. It is called at two times: first render and first data update. This article only analyzes the first rendering. The _update method renders the VNode as a real DOM. Its definition in the SRC/core/instance/liefcycle js file:

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

The core is to call the vm.__patch__ method, which has different definitions in different platforms. The definition in the Web platform is as follows:

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

So in the browser rendering, pointing to the patch method, it is defined in SRC/platforms/web/runtime/patch. Js file:

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

This method is defined as the value returned by calling createPatchFunction. CreatePatchFunction eventually returns a patch method, which is assigned to vM. __Patch__. Let’s look at the implementation of the createPatchFunction method:

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

  // ...

  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

Back to the patch method itself, it takes four arguments. OldVnode represents the oldVnode, which may not exist or is a DOM object. Vnode represents the vnode returned by _render; Hydrating indicates whether it is server side rendering. RemoveOnly is for transition-group.

The following uses an example to analyze its execution logic.

var app = new Vue({
  el: '#app'.render: function (createElement) {
    return createElement('div', { attrs: { id: 'app'}},this.message)
  },
  data: { message: 'Hello Vue! '}})Copy the code

The patch method is called in the vm._update method:

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

$el is assigned to the mountComponent function, vnode is returned by the render function, hydrating is false for non-server rendering, and removeOnly is false.

Then go back to the execution process of patch function and see several key steps:

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

Since the oldVnode we passed in is actually a DOM Container, isRealElement is true. We then convert the oldVnode into a VNode object using emptyNodeAt. Then call the createElm method. CreateElm is important here, so let’s look at its implementation:

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

CreateElm creates a real DOM from a virtual node and inserts it into its parent node. Let’s look at some of its key logic. The purpose of the createComponent method is to try to create a child component, and in this case it returns false; Then check whether the VNode contains a tag. If so, verify the validity of the tag in a non-production environment to see if it is a valid tag. Then call the platform DOM operation to create a placeholder element.

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

Next call the createChildren method to create child elements:

createChildren(vnode, children, insertedVnodeQueue)

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

CreateChildren’s logic is simple, and it actually iterates over the child virtual nodes, recursively calling createElm, a common depth-first traversal algorithm. It is important to note that vNode. elm is passed in as a DOM node placeholder for the parent.

Then call the invokeCreateHooks method to execute all the CREATE hooks and push the vNode to insertedVnodeQueue.

if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}

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

Finally, insert is called to insert the DOM into the parent node. Since it is a recursive call, the child element will call INSERT first, so the insertion order of the nodes in the entire VNode tree is child before parent.

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

Insert logic is simple, call some nodeOps traverse to the child node is inserted into the parent node, auxiliary methods defined in SRC/platforms/web/runtime/node – ops. Js file:

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

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.

Back to the Patch method, the first render calls createElm, where parentElm is the parent element of oldvNode.elm. Essentially, the whole process is to recursively create an entire DOM tree and insert it into the Body.

Finally, the vNode inserts the sequential queue from the previous recursive createElm, and the associated INSERT hook function is executed.

Six, summarized

This completes the analysis of how the template and data are rendered into the final DOM from the main line, and the following figure gives a more intuitive view of the entire process from Vue initialization to final rendering.

This article has only analyzed the most basic and simplest scenarios. In practice, pages are broken down into many components, so the componentization process will be analyzed in the next article.