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.
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.
Xueersi Online School Tech Team
The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.