Creation of the virtual DOM

What is the virtual DOM

A Virtual DOM is a Virtual DOM node. It simulates the DOM nodes through JS objects, and then renders the virtual DOM into real DOM nodes through specific render methods

Why use the virtual DOM

The virtual DOM is an effort to solve performance problems caused by frequent manipulation of DOM elements. Manipulating DOM elements with JS scripts causes backflow or redrawing of the browser. Let’s talk a little bit about the concept of backflow and redraw:

  • Backflow: When our changes to a DOM element cause the element’s size to change, the browser recalculates the size and position of the element and eventually draws the recalculated result onto the screen, a process called backflow

  • Redraw: When we change the value of a DOM element to only change the color of the element, the browser does not need to recalculate the size and position of the element, but simply redraw the new style. This process is called redrawing

Obviously, refluxing is more performance costly than painting. When using the virtual DOM, we will operate on the virtual DOM first. The virtual DOM will combine multiple changes into a batch operation, so as to reduce the number of DOM rearrangement and shorten the time spent in generating rendering trees and drawing.

Virtual DOM in Vue

In Vue, a constructor called VNode is used to describe a DOM node.

VNode constructor

  /** * Vnode constructor *@param {*} tag 
   * @param {*} data 
   * @param {*} children 
   * @param {*} text 
   * @param {*} elm 
   * @param {*} context 
   * @param {*} componentOptions 
   * @param {*} asyncFactory 
   */
  var VNode = function VNode (tag, data, children, text, elm, context, componentOptions, asyncFactory) {
    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;
  };
Copy the code

Vue uses the VNode constructor to describe DOM nodes. Here’s how to create comment nodes and text nodes

Creating a comment node

/** * Create comment node (empty node) *@param {*} text 
 * @returns * /
export const createEmptyVNode = (text: string = ' ') = > {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
Copy the code

Creating a text node

/** * Create text node *@param {*} val 
 * @returns * /
export function createTextVNode (val: string | number) {
  return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code

Creation of the virtual DOM

In the mount process of Vue, after obtaining the render function, call vm._render method, convert the render function into virtual DOM, see the implementation of vM. _render method

// /core/instance/render.js
// Convert the Vue instance into a virtual DOM
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 becaues 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 create and generate the virtual DOM, using the $createElement method as the first argument to the render function, the same as the handwritten render function
    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

As you can see, the core of the _render method is vnode = render. Call (vm._renderProxy, vm.$createElement), which converts the render function into a virtual DOM. We’ll think about what happens when we write the render function by hand, so let’s look at this example

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // Label name
      this.$slots.default // Array of child nodes)},props: {
    level: {
      type: Number.required: true}}})Copy the code

$createElement (vm.$createElement); $createElement (vm.$createElement); CreateElement encapsulates _createElement.

// 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> {
  /** * The third argument is an object (the data option is usually an object) */
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // Distinguish between the handwritten render method and the internal template compiled template method
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  // Attributes in the data option cannot use reactive objects
  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
  }
  if(! tag) {// in case of component :is set to falsy value
    // Prevent dynamic components from returning an empty node when the is property is false
    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
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    / / handwritten render
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // template compiles the render function
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // Check whether it is a built-in tag, such as an HTML tag in a browser
    if (config.isReservedTag(tag)) {

      // platform built-in elements
      // Create a virtual DOM directly with built-in tags
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // component
      // Create a VNode of component type for a registered custom 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 is used to implement the _createElement method, and the _createElement method is used to validate the data

Data specification check

    1. dataCannot use reactive objects as properties in
// Attributes in the data option cannot use reactive objects
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()
}
Copy the code
    1. When a particular propertykeyIs a non-original data type, such as a non-string or a non-numeric type
// 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
    )
  }
}
Copy the code

Child node normalization

The next step is to normalize the child nodes. The virtual DOM is a virtual DOM tree composed of each VNode in the form of a tree, so we need to ensure that every byte point is a VNode type. Here we need to analyze the two sources of the _render function separately

  • user-definedrenderFunction.

When normalizing the user-defined render function, if the childrenNode is an array (for example, children with V-for), it needs to be traversed; If the array still exists, recurse

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

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
  • Compiled by the templaterenderfunction

The render functions compiled from the template are all of type VNode (the functional component is an array, more on that later), so we only need to convert the entire children to a one-dimensional array

/** * flattens the array to a one-dimensional array *@param {*} children 
 * @returns * /
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