Virtual DOM

The concept of Virtual DOM is more or less familiar to anyone who knows or has used Vue, React and other MVVM frameworks. DOM in browsers is “expensive”, and there are certain performance problems when we frequently update DOM:

let div = document.createElement('div');
let str = ""
for(let key in div) {
    str+=key+""
}
console.log(str)
Copy the code

The code above gives you a look at the design inside the DOM. If you are interested in running it, you will find that DOM design is very complex, which is why Virtual DOM comes into being.

In essence, Virtual DOM can be regarded as a mapping of DOM nodes on the browser in JavaScript. A DOM node is described by native JS objects. In Vue, the Virtual DOM is described using the vnode Class, which is defined in the SRC /core/vdom/vnode.js file:

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodesfnOptions: ? ComponentOptions;// for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtoolsfnScopeId: ? string;// functional scope id support

  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

Although Vue’s definition of Virtual DOM is somewhat complex, because it contains many features of Vue, its core is nothing more than a few key attributes, tag names, data, child nodes, key values, etc., other attributes are used to extend the flexibility of VNode. Since VNode is just a rendering that maps to the real DOM, it doesn’t need to include methods to manipulate the DOM, so it’s very lightweight and simple.

In addition to the definition of its data structure, the Virtual DOM needs to go through the process of VNode create, diff, patch and so on when mapping to the real DOM.

In Vue, the Virtual DOM is created using the previously enhanced createElement method, which is defined in the SRC /core/vdom/create-element.js file:

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 for more flexibility in the parameters passed in. After processing these parameters, call _createElement, the function that actually creates VNode:

export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  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
    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) {
    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))) {
      // 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. It is a VNodeData type and can be found 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.

We will focus on two key processes: —————— children processing and VNode creation.

childrenThe processing of


The Virtual DOM is actually a tree structure, and each VNode may have several child nodes, which should also be the type of the VNode. The fourth argument received by _createElement is children of any type, so we need to treat them as vNodes. The normalizeChildren(children) and simpleNormalizeChildren(children) methods are called, depending on the normalizationType. Their definitions are in SRC/core/vdom/helpers/normalzie – children. In 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
}

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

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. The normalizeChildren method has two cases. The first case is that the render function is written by the user. When children have only one node, Vue. In this case createTextVNode is called to create a VNode of a text node; The normalizeArrayChildren method is called when slot, V-for is compiled to generate a nested array:

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 for the child node to process and nestedIndex for the nestedIndex, since a single child can be an array type. So when we get a single node C, we do a type check, and if it’s an array, we recursively call normalizeArrayChildren; If it is a base type, convert createTextVNode to vNode directly, and update its key according to nestedIndex if multiple lists are nested. And if there are consecutive text nodes during the traversal, they are merged into a single node. Finally, children becomes an Array of type VNode.

The creation of a VNode

The createElement function, after processing children, goes back and creates a VNode instance:

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. Otherwise, create a VNode with an unknown label. Create a Component VNode if the tag is not string or Component.

conclusion

So here we have the general ideacreateElementThe process of creating a VNode is required for each VNodechildren.childernThe elements of the DOM Tree are vNodes, and then form a VNode Tree corresponding to the DOM Tree.

Vue2 source code analysis (four) vue2 source code analysis (three) VUE2 source code analysis (two) VUe2 source code analysis (a)