Frontend Development 14 min read

Understanding Vue's Virtual DOM: VNode Creation, Rendering Flow, and Core Utilities

This article explains how Vue 2 implements a virtual DOM by describing the structure of VNode objects, the render function, the createElement process, and the normalization utilities, providing detailed code examples and step‑by‑step analysis of the rendering pipeline.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Understanding Vue's Virtual DOM: VNode Creation, Rendering Flow, and Core Utilities

Vue 2 introduced a virtual DOM, which represents real DOM elements as JavaScript objects called VNodes. The article starts by showing a simple HTML snippet and its equivalent render function using the h (or $createElement ) helper.

It then presents the source of the VNode class, highlighting properties such as tag , data , children , text , and various flags used internally by Vue.

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;
    // ... other flags omitted for brevity
  }
  get child (): Component | void {
    return this.componentInstance;
  }
}

Utility functions for creating specific VNode types are shown, including createEmptyVNode , createTextVNode , and cloneVNode , each returning a properly configured VNode instance.

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode();
  node.text = text;
  node.isComment = true;
  return node;
};

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val));
}

export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    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;
}

The article then walks through the rendering process: the component’s render function returns a VNode, which is produced by calling vm._render() . The _render method retrieves the user‑defined render function from vm.$options and invokes it with vm.$createElement (the h helper).

Vue.prototype._render = function (): VNode {
  const vm: Component = this;
  const { render, _parentVnode } = vm.$options;
  let vnode;
  try {
    currentRenderingInstance = vm;
    vnode = render.call(vm._renderProxy, vm.$createElement);
  } catch (e) {
    // error handling omitted
  }
  return vnode;
};

The createElement function normalizes arguments and forwards the call to _createElement , which performs checks, handles scoped slots, and finally constructs a VNode for element tags or returns an empty VNode for invalid cases.

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
) {
  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);
}

Inside _createElement , after handling reactive data and component tags, the function normalizes children using either normalizeChildren (for always‑normalize) or simpleNormalizeChildren . The core of this step is the normalizeArrayChildren routine, which recursively flattens nested child arrays, merges adjacent text nodes, and assigns keys for v‑for generated lists.

function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
  const res = [];
  for (let i = 0; i < children.length; i++) {
    let c = children[i];
    if (isUndef(c) || typeof c === 'boolean') continue;
    const lastIndex = res.length - 1;
    const last = res[lastIndex];
    if (Array.isArray(c)) {
      if (c.length > 0) {
        c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`);
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + c[0].text);
          c.shift();
        }
        res.push.apply(res, c);
      }
    } else if (isPrimitive(c)) {
      if (isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c);
      } else if (c !== '') {
        res.push(createTextVNode(c));
      }
    } else {
      if (isTextNode(c) && isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c.text);
      } else {
        if (isTrue(children._isVList) && isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__`;
        }
        res.push(c);
      }
    }
  }
  return res;
}

By flattening the child arrays, Vue ensures a simple, linear VNode tree that mirrors the intended DOM hierarchy, allowing vm._update to efficiently patch the real DOM. The article concludes by noting that component VNodes and other advanced cases will be covered in future installments.

FrontendJavaScriptVuevirtual DOMVNodeRender Function
Xueersi Online School Tech Team
Written by

Xueersi Online School Tech Team

The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.