Why use the Virtual DOM

  • Manipulating the DOM manually is cumbersome, and browser compatibility issues need to be considered. Although JQuery simplifies DOM manipulation, the complexity of DOM manipulation increases as projects become more complex.
  • In order to simplify the complex manipulation of DOM, various MVVM frameworks have emerged to solve the problem of view and state synchronization
  • We can use a template engine to simplify view operations, but the template engine does not solve the problem of tracking state changes
  • The nice thing about Virtual DOM is that you don’t need to update the DOM immediately when the state changes. You just need to create a Virtual tree to describe the DOM

Back in the JQuery era, we used to render pages using JS to manipulate the Dom and then mount data to the Dom. As we know from many front-end accounts and books, manipulating the DOM is expensive. It is often not the JS logic that affects front-end performance, but the operation to update the DOM. Why does manipulating the DOM affect performance? First let’s try to get a property on a div

// DOM
<div class="box"></div>
Copy the code
// js
const div = document.querySelector('.box')

let a = ' '
for(const prop in div) {
  a = a + prop + ' '
}
console.log(a)
Copy the code

If we were to manipulate hundreds or thousands of DOM elements, on the one hand we would have to evaluate the DOM with these attributes, and on the other hand we would have to rearrange and redraw the browser engine to modify the DOM, which would cost performance. So we had to figure out if we could map the DOM to objects, mount the processed data to those objects, and then compare the differences with the previous objects so that we could update only the ONES we needed to change, and the virtual DOM was born.

Virtual DOM in Vue

Vue: SRC /core/vdom/vnode.js: SRC /core/vdom/vnode.js: SRC /core/vdom/vnode.js

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
  ) {
    /* Label name of the current node */
    this.tag = tag
    /* The object corresponding to the current node, which contains specific data information, is a VNodeData type, you can refer to the VNodeData type data information */
    this.data = data
    /* The child of the current node is an array */
    this.children = children
    /* The text of the current node */
    this.text = text
    /* The actual DOM node corresponding to the current virtual node */
    this.elm = elm
    /* Namespace of the current node */
    this.ns = undefined
    /* The compile scope of the current node */
    this.context = context
    /* Functional component scope */
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    /* The key attribute of the node, which is used as the node's identifier, is used to optimize */
    this.key = data && data.key
    /* The component's option */
    this.componentOptions = componentOptions
    /* The instance of the component corresponding to the current node */
    this.componentInstance = undefined
    /* The parent of the current node */
    this.parent = undefined
    InnerHTML is true, and textContent is false*/
    this.raw = false
    /* Whether the node is static */
    this.isStatic = false
    /* Whether to insert as the heel node */
    this.isRootInsert = true
    /* Is a comment node */
    this.isComment = false
    /* Whether the node is a clone */
    this.isCloned = false
    /* Whether there is a v-once instruction */
    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

These are the attributes of Vnode. If you look at them at first, you might be a little confused. What does it do? Now we represent the DOM with a virtual node

{
    tag: 'div'
    data: {
        class: 'v-div'
    },
    children: [{tag: 'span'.data: {
                class: 'v-span'
            }
            text: 'hello, VNode'}}]Copy the code

The Vnode above is rendered like this

<div class="v-div">
    <span class="v-span">hello, VNode</span>
</div>
Copy the code

Method to generate a Vnode

  • Create an empty Vnode
export const createEmptyVNode = (text: string = ' ') = > {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
Copy the code
  • Create a text node
export function createTextVNode (val: string | number) {
  return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code
  • Clone a Vnode
export function cloneVNode (vnode: VNode) :VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    / / # 7975
    // clone children array to avoid mutating original in case of cloning
    // a child.vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isComment = vnode.isComment cloned.fnContext = vnode.fnContext cloned.fnOptions = vnode.fnOptions cloned.fnScopeId = vnode.fnScopeId  cloned.asyncMeta = vnode.asyncMeta cloned.isCloned =true
  return cloned
}
Copy the code
  • CreateComponent Creates a component node
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)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  /* If Ctor is still not a constructor or an asynchronous component factory at this stage, return */
  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
  installComponentHooks(data)

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

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}
Copy the code
  • createElement
export function createElement (context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean) :VNode | Array<VNode> {
  /* Data is not transmitted */
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  /* If alwaysNormalize is true, normalizationType is marked as ALWAYS_NORMALIZE*/
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  /* Create a virtual node */
  return _createElement(context, tag, data, children, normalizationType)
}

/* Create a VNode */
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 (which represents the Oberver object binding), https://cn.vuejs.org/v2/guide/render-function.html# constraints 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
  /* Default default scope 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
    /* Get the namespace of the tag */
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    /* Check whether the label is reserved */
    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
        )
      }
      /* If it is a reserved label, create a corresponding node */
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined.undefined, context
      )
    } else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options,'components', tag))) {
      // component
      /* 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 */
      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
      /* 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 there is a namespace, it is applied recursively to all child nodes */
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    /* If the vNode is not successfully created, create an empty node */
    return createEmptyVNode()
  }
}
Copy the code
Copy the code