In Vue 3's renderer, what is a block, what does its dynamicChildren array hold, and why do v-if and v-for start new blocks?
answer
- openBlock then createElementBlock
- stable inner structure
- flat list of dynamic descendants
- patched pairwise by index
- structure changes need their own block
basics
~20 sA block is a template region with stable structure; its dynamicChildren array is a flat list of every descendant with a patch flag plus components. Updates patch that list pairwise, so v-if and v-for, which change structure, get their own blocks.
solid answer
~40 s`openBlock()` starts collecting, and `createElementBlock()` stores every descendant vnode created meanwhile that has a positive patch flag, plus every component vnode, as the block's flat `dynamicChildren` array, at any depth. On update, `patchBlockChildren` walks the old and new arrays pairwise by index, so static wrappers and attributes are never visited. This is tree flattening. Pairwise matching needs the same dynamic nodes in the same order, which structural directives break. So each `v-if` branch is its own keyed block, tracked as one entry by its parent, and a `v-for` becomes a fragment opened with tracking disabled whose items are diffed as a keyed or unkeyed list. If block lengths ever disagree, the renderer falls back to a full diff. Hand-written render functions open no blocks at all.
code
ts · 15 lines// Compiled from:
// <div>
// <section><p>{{ a }}</p></section>
// <p v-if="ok">{{ b }}</p>
// </div>
// (abridged; exact identifiers vary with the compile mode)
(_openBlock(), _createElementBlock('div', null, [
_createElementVNode('section', null, [
_createElementVNode('p', null, _toDisplayString(_ctx.a), 1 /* TEXT */)
]),
_ctx.ok
? (_openBlock(), _createElementBlock('p', { key: 0 }, _toDisplayString(_ctx.b), 1 /* TEXT */))
: _createCommentVNode('v-if', true)
]))
// root block dynamicChildren: [ <p>{{ a }}</p>, the v-if branch (block or comment) ]go deeper
Recall that Vue tracks only the dynamic parts of a template so updates can skip the static parts.
Explain openBlock and createElementBlock, the flat dynamicChildren list, and how patching walks it pairwise.
Explain why v-if, v-for and dynamic keys need their own blocks, and recognize the full-diff fallbacks when block structure is unstable.
Relate tree flattening to decisions about template-first authoring and hydration cost in large server-rendered applications.
## What a block is The Vue docs define a **block** as a part of the template with a **stable inner structure**: the same nodes, in the same positions, on every render. In compiled output a block is created with a pair of calls: ```js (_openBlock(), _createElementBlock("div", null, [ /* children */ ])) ``` - `openBlock()` pushes a fresh array onto a stack of open blocks. - While the children are being created, every vnode that has a **positive patch flag**, and every **component** vnode, is pushed into the current open array. Components are always tracked, because even a component that does not need to update must hand its instance on to the new vnode. - `createElementBlock()` then closes the block and stores the array on the block vnode as **`dynamicChildren`**. The block itself is pushed into its parent block's array, so blocks nest. The array is **flat**: it contains dynamic descendants from any depth, not just direct children. Static wrappers in between simply do not appear. This is **tree flattening**. ## How patching uses it On update, if the new vnode has `dynamicChildren`, the renderer runs `patchBlockChildren`: it walks the old and new arrays **pairwise by index** and patches each pair, typically on the fast path chosen by the vnode's patch flag. Everything that is not in the array (static wrappers, static text, static attributes) is never visited. ```html <div> <!-- root block --> <div>...</div> <!-- not tracked --> <div :id="id"></div> <!-- tracked --> <div> <!-- not tracked --> <div>{{ bar }}</div> <!-- tracked --> </div> </div> ``` Here the root block's `dynamicChildren` is just two elements, however deep and wide the static parts are. ## Why structural directives start new blocks Pairwise matching by index is only valid if the old and new arrays describe the **same set of dynamic nodes in the same order**. Structural directives break that: 1. **`v-if` / `v-else`** switch between branches that contain different nodes. The compiler therefore makes each branch its own block, with a distinct `key` (0, 1, 2, ...). The parent block tracks the branch block as a single entry, so the parent's array keeps a stable shape; when the branch changes, the different key means the old block is replaced rather than matched node by node. 2. **`v-for`** produces a variable number of items. The list becomes a fragment created with `openBlock(true)`, which disables collection inside it; its items are diffed by the keyed or unkeyed children algorithm (flag `KEYED_FRAGMENT` or `UNKEYED_FRAGMENT`), and each item is typically its own block. 3. An element with a dynamic `key` also becomes its own block, since a changed key means a different node. ## Safety valves - If an old and new block disagree on the length of `dynamicChildren`, the renderer abandons the fast path and does a **full diff** for that element. - A `-2 /* BAIL */` patch flag turns off optimized mode for a subtree. - A **hand-written render function** does not open blocks, so its vnodes have no `dynamicChildren` and are fully diffed. ## A worked trace Take the compiled example in this answer's code sample: a `<section>` wrapping a `<p>{{ a }}</p>`, followed by a `<p v-if="ok">{{ b }}</p>`. 1. `openBlock()` opens the root array. 2. The `section` is created without a flag, so it is not collected; the inner `<p>` carries `1 /* TEXT */` and is pushed. 3. The `v-if` branch opens its own block, closes it, and pushes itself as one entry; when `ok` is false, a comment placeholder is created instead. 4. `createElementBlock('div', ...)` stores the two collected entries as `dynamicChildren`. 5. On the next update, the renderer patches those two entries pairwise and never visits the `section`. ## Why this matters beyond updates The docs note that patch flags and tree flattening also speed up **SSR hydration**: only block nodes and their dynamic descendants need to be traversed, which amounts to partial hydration at the template level. ## Mapping it back to templates | Template construct | Block behaviour | |---|---| | component root template | one root block | | `v-if` / `v-else-if` / `v-else` branch | its own keyed block, tracked by the parent | | `v-for` | fragment block with tracking disabled; items diffed as a list | | element with dynamic `key` | its own block | | static wrapper element | not tracked at all |
- Why are component vnodes always added to a block's dynamicChildren, even without a patch flag?Even when a child component does not need to re-render, the renderer must process its vnode on each parent update to carry the component instance over to the new vnode, so it can later be updated or unmounted correctly. Tracking every component in the block guarantees that step is never skipped.
- What does the renderer do if an old and a new block have different dynamicChildren lengths?It treats the block metadata as unreliable and abandons the optimized path for that element: patch flag and `dynamicChildren` are ignored and the children and props are fully diffed. That guard protects against blocks whose structure was not actually stable, for example vnodes wrapped by hand.
A block is like a checklist clipped to the front of a thick binder: it lists only the pages that ever get edited, so a reviewer flips straight to those, and any section whose page count can change gets its own separate checklist.
saying these in an interview costs you the question
- Thinks dynamicChildren holds only an element's direct children
- Believes a v-if branch is diffed node by node against the other branch
- Says v-for items are collected into the parent's dynamicChildren
- Claims hand-written render functions build blocks too
- Assumes components without patch flags are left out of blocks