Virtual DOM II: Deserialize
advancedRebuild real DOM from the virtual tree, completing the render pipeline.
Serialize real DOM into a virtual DOM tree in JavaScript — node-type dispatch, attributes vs properties, and what React's element objects actually contain.
Write
serialize(element): convert a real DOM subtree into a tree of plain objects —{ type, props, children }— that fully describes it.
This is part one of a two-part question (part two rebuilds real DOM from your objects; the round-trip must be lossless). Interviewers like the pair because it is the core representation trick of React and Vue, stripped of framework machinery: UI as data. A React element — what JSX compiles to — is almost exactly the object you're about to produce, and saying so up front reframes the exercise from "DOM trivia" to "reimplementing the industry's core idea."
The algorithmic content is a DFS over a tree with node-type dispatch — the same traversal as deep clone, pointed at a different tree.
function serialize(node) {
// Text nodes are leaves: no tag, no attributes, just characters
if (node.nodeType === Node.TEXT_NODE) {
return {
type: "text",
props: { nodeValue: node.nodeValue },
children: [],
};
}
// Skip comments, processing instructions, etc.
if (node.nodeType !== Node.ELEMENT_NODE) {
return null;
}
const vNode = {
type: node.tagName.toLowerCase(), // normalize: DOM reports "DIV"
props: {},
children: [],
};
// Attributes as written in markup — id, class, data-*, aria-*
for (const attr of node.attributes) {
vNode.props[attr.name] = attr.value;
}
// childNodes, NOT children — we need text nodes too
for (const child of node.childNodes) {
const serialized = serialize(child);
if (serialized !== null) vNode.children.push(serialized);
}
return vNode;
}const doc = new DOMParser().parseFromString(
`<div id="app"><h1 class="title">Hello!</h1><p>Welcome.</p></div>`,
"text/html"
);
serialize(doc.getElementById("app"));
// {
// type: "div", props: { id: "app" }, children: [
// { type: "h1", props: { class: "title" }, children: [
// { type: "text", props: { nodeValue: "Hello!" }, children: [] } ] },
// { type: "p", props: {}, children: [
// { type: "text", props: { nodeValue: "Welcome." }, children: [] } ] } ]
// }childNodes vs children — children yields only elements; text lives in text nodes, and using children silently produces a tree with no words in it. This is the planted bug, and it's brutal because the structure looks right in the console.tagName is uppercase ("DIV") for HTML — normalize on the way in or compare case-insensitively forever after."\n " text nodes; your serializer faithfully keeps them. Filtering them is a policy (React's JSX compiler trims some whitespace at compile time) — name the choice rather than being surprised by the noise.attr.value reads the markup attribute; live DOM state can differ (input.value after typing vs its value attribute; checked likewise). A serializer of markup uses attributes; a serializer of current UI state would need property reads for form elements. Knowing the two layers exist is the senior distinction here — it returns with force in part two.addEventListener are invisible to any DOM traversal — there is no API to enumerate them. So a serialized tree describes structure, not behavior — precisely why React keeps handlers in its element objects (onClick: fn as a prop, never in the DOM) and why "serialize the page and send it elsewhere" schemes always lose interactivity.// Your vNode: // React's element (roughly):
{ type: "h1", { type: "h1",
props: { class: "title" }, props: { className: "title",
children: [ ... ] } children: [ ... ] },
key: null, ref: null }Differences worth reciting: React puts children inside props; uses className/htmlFor (property names, since class was a reserved word); adds key (identity for list diffing) and allows type to be a function — a component — which is the exact point where a template language becomes a component model. Your serializer goes DOM→object; React's render goes object→DOM — you're building the return leg in part two.
null, filtered by the caller); some VDOMs keep them as a comment type (Vue does, for placeholder anchors). Policy, stated.createElementNS — flag it now, handle it in part two.<input disabled> yields disabled: "" (empty string, present = true); faithfully round-trippable, semantically weird — know why.iframe/shadowRoot boundaries — traversal stops at document/shadow boundaries; a serializer that claims "the whole page" without mentioning them is overclaiming.children instead of childNodes (the wordless tree).node.className) while claiming to serialize attributes — mixing the two layers without knowing it.<pre>, inline spacing).data-*/aria-* — they're ordinary attributes; the special-case code is a tell of pattern-matching over understanding.outerHTML / server-side rendering; discuss escaping (<) and why string-building for the browser lost to object-building (JSON.stringify's escaping lesson rhymes).serialize → deserialize → serialize and deep-equal the two vNode trees — property-based round-trip testing, the same oracle trick as in curry-with-placeholders.Rebuild real DOM from the virtual tree, completing the render pipeline.
Model back/forward/push navigation with an index and a stack — the core of every router.
Diff two virtual trees and apply minimal DOM mutations — the reconciliation step that makes the virtual DOM worth having.
Implement delegate(root, selector, handler) with closest() — one listener for a thousand rows, and why frameworks did exactly this.