v-for Directive
Consider the following component:
<script setup>
import { ref } from "vue";
const items = ref([
{ id: 1, name: "a" },
{ id: 2, name: "b" },
{ id: 3, name: "c" },
]);
</script>
<template>
<div v-for="item in items" :key="item.id">{{ item.name }}</div>
</template>Compilation Result and Overview
The compilation result is as follows:
import {
renderEffect as _renderEffect,
setText as _setText,
createFor as _createFor,
template as _template,
} from "vue/vapor";
const t0 = _template("<div></div>");
function _sfc_render(_ctx) {
const n0 = _createFor(
() => _ctx.items,
(_ctx0) => {
const n2 = t0();
_renderEffect(() => _setText(n2, _ctx0[0].name));
return n2;
},
(item) => item.id
);
return n0;
}createFor takes the following arguments:
- source: The source to loop over (
() => _ctx.items) - renderItem: The function to render each item
- getKey: The function to get the key (optional)
_ctx0 is an array of [item, key, index], where _ctx0[0] is item, _ctx0[1] is key, and _ctx0[2] is index.
Various Patterns
Nested v-for
<template>
<div v-for="item in list">
<span v-for="child in item">{{ child + item }}</span>
</div>
</template>const n0 = _createFor(
() => _ctx.list,
(_ctx0) => {
const n5 = t1();
const n2 = _createFor(
() => _ctx0[0],
(_ctx1) => {
const n4 = t0();
_renderEffect(() => _setText(n4, _ctx1[0] + _ctx0[0]));
return n4;
}
);
_insert(n2, n5);
return n5;
}
);When nested, _ctx0 refers to the outer v-for's context, and _ctx1 refers to the inner v-for's context.
Destructuring
<template>
<div v-for="{ id, ...other } in list" :key="id">
{{ id + other }}
</div>
</template>const n0 = _createFor(
() => _ctx.list,
_withDestructure(
([{ id, ...other }, index]) => [id, other, index],
(_ctx0) => {
const n2 = t0();
_renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]));
return n2;
}
),
({ id, ...other }, index) => id
);For destructuring, the withDestructure helper is used to convert the destructured values into an array.
Reading the Compiler
IR
First, let's look at the IR for v-for.
75export interface IRFor {
76 source: SimpleExpressionNode
77 value?: SimpleExpressionNode
78 key?: SimpleExpressionNode
79 index?: SimpleExpressionNode
80}
81
82export interface ForIRNode extends BaseIRNode, IRFor {
83 type: IRNodeTypes.FOR
84 id: number
85 keyProp?: SimpleExpressionNode
86 render: BlockIRNode
87 once: boolean
88}export interface IRFor {
source: SimpleExpressionNode;
value?: SimpleExpressionNode;
key?: SimpleExpressionNode;
index?: SimpleExpressionNode;
}
export interface ForIRNode extends BaseIRNode, IRFor {
type: IRNodeTypes.FOR;
id: number;
keyProp?: SimpleExpressionNode;
render: BlockIRNode;
once: boolean;
}source: The source expression to loop overvalue: The loop variable (item)key: The key variable (for object loops)index: The index variablekeyProp: The:keyprop expressionrender: The rendering block for each itemonce: When combined withv-once
Transformer
transformVFor is defined using createStructuralDirectiveTransform.
21export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
22 'for',
23 processFor,
24)
25
26export function processFor(
27 node: ElementNode,
28 dir: VaporDirectiveNode,
29 context: TransformContext<ElementNode>,
30) {
31 if (!dir.exp) {
32 context.options.onError(
33 createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc),
34 )
35 return
36 }
37 const parseResult = dir.forParseResult
38 if (!parseResult) {
39 context.options.onError(
40 createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc),
41 )
42 return
43 }
44
45 const { source, value, key, index } = parseResult
46
47 const keyProp = findProp(node, 'key')
48 const keyProperty = keyProp && propToExpression(keyProp)
49 context.node = node = wrapTemplate(node, ['for'])
50 context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
51 const id = context.reference()
52 const render: BlockIRNode = newBlock(node)
53 const exitBlock = context.enterBlock(render, true)
54 context.reference()
55
56 return (): void => {
57 exitBlock()
58 context.registerOperation({
59 type: IRNodeTypes.FOR,
60 id,
61 source: source as SimpleExpressionNode,
62 value: value as SimpleExpressionNode | undefined,
63 key: key as SimpleExpressionNode | undefined,
64 index: index as SimpleExpressionNode | undefined,
65 keyProp: keyProperty,
66 render,
67 once: context.inVOnce,
68 })
69 }
70}export const transformVFor: NodeTransform = createStructuralDirectiveTransform(
"for",
processFor
);
export function processFor(
node: ElementNode,
dir: VaporDirectiveNode,
context: TransformContext<ElementNode>
) {
// ...
const parseResult = dir.forParseResult;
const { source, value, key, index } = parseResult;
const keyProp = findProp(node, "key");
const keyProperty = keyProp && propToExpression(keyProp);
// ...
const render: BlockIRNode = newBlock(node);
const exitBlock = context.enterBlock(render, true);
return (): void => {
exitBlock();
context.registerOperation({
type: IRNodeTypes.FOR,
id,
source: source as SimpleExpressionNode,
value: value as SimpleExpressionNode | undefined,
key: key as SimpleExpressionNode | undefined,
index: index as SimpleExpressionNode | undefined,
keyProp: keyProperty,
render,
once: context.inVOnce,
});
};
}forParseResult is the result parsed by the @vue/compiler-dom parser, containing source, value, key, and index.
Codegen
The genFor function handles code generation.
14export function genFor(
15 oper: ForIRNode,
16 context: CodegenContext,
17): CodeFragment[] {
18 const { vaporHelper } = context
19 const { source, value, key, index, render, keyProp, once, id } = oper
20
21 let isDestructureAssignment = false
22 let rawValue: string | null = null
23 const rawKey = key && key.content
24 const rawIndex = index && index.content
25
26 const sourceExpr = ['() => (', ...genExpression(source, context), ')']
27
28 const idsOfValue = new Set<string>()
29 if (value) {
30 rawValue = value && value.content
31 if ((isDestructureAssignment = !!value.ast)) {
32 walkIdentifiers(
33 value.ast,
34 (id, _, __, ___, isLocal) => {
35 if (isLocal) idsOfValue.add(id.name)
36 },
37 true,
38 )
39 } else {
40 idsOfValue.add(rawValue)
41 }
42 }
43
44 const [depth, exitScope] = context.enterScope()
45 let propsName: string
46 const idMap: Record<string, string | null> = {}
47 if (context.options.prefixIdentifiers) {
48 propsName = `_ctx${depth}`
49 Array.from(idsOfValue).forEach(
50 (id, idIndex) => (idMap[id] = `${propsName}[${idIndex}]`),
51 )
52 if (rawKey) idMap[rawKey] = `${propsName}[${idsOfValue.size}]`
53 if (rawIndex) idMap[rawIndex] = `${propsName}[${idsOfValue.size + 1}]`
54 } else {
55 propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
56 }
57
58 let blockFn = context.withId(
59 () => genBlock(render, context, [propsName]),
60 idMap,
61 )
62 exitScope()
63
64 let getKeyFn: CodeFragment[] | false = false
65 if (keyProp) {
66 const idMap: Record<string, null> = {}
67 if (rawKey) idMap[rawKey] = null
68 if (rawIndex) idMap[rawIndex] = null
69 idsOfValue.forEach(id => (idMap[id] = null))
70
71 const expr = context.withId(() => genExpression(keyProp, context), idMap)
72 getKeyFn = [
73 ...genMulti(
74 ['(', ')', ', '],
75 rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
76 rawKey ? rawKey : rawIndex ? '__' : undefined,
77 rawIndex,
78 ),
79 ' => (',
80 ...expr,
81 ')',
82 ]
83 }
84
85 if (isDestructureAssignment) {
86 const idMap: Record<string, null> = {}
87 idsOfValue.forEach(id => (idMap[id] = null))
88 if (rawKey) idMap[rawKey] = null
89 if (rawIndex) idMap[rawIndex] = null
90 const destructureAssignmentFn: CodeFragment[] = [
91 '(',
92 ...genMulti(
93 DELIMITERS_ARRAY,
94 rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
95 rawKey ? rawKey : rawIndex ? '__' : undefined,
96 rawIndex,
97 ),
98 ') => ',
99 ...genMulti(DELIMITERS_ARRAY, ...idsOfValue, rawKey, rawIndex),
100 ]
101
102 blockFn = genCall(
103 vaporHelper('withDestructure'),
104 destructureAssignmentFn,
105 blockFn,
106 )
107 }
108
109 return [
110 NEWLINE,
111 `const n${id} = `,
112 ...genCall(
113 vaporHelper('createFor'),
114 sourceExpr,
115 blockFn,
116 getKeyFn,
117 false, // todo: getMemo
118 false, // todo: hydrationNode
119 once && 'true',
120 ),
121 ]
122}Key processes:
- Source expression generation: In the form
() => (_ctx.items) - Render function generation: A function that takes
_ctx0as argument - Key function generation: When
:keyprop exists - Destructuring handling: Using
withDestructure
if (isDestructureAssignment) {
blockFn = genCall(
vaporHelper("withDestructure"),
destructureAssignmentFn,
blockFn
);
}
return [
NEWLINE,
`const n${id} = `,
...genCall(
vaporHelper("createFor"),
sourceExpr,
blockFn,
getKeyFn,
false, // getMemo
false, // hydrationNode
once && "true"
),
];Reading the Runtime
createFor is implemented in apiCreateFor.ts in runtime-vapor.
44export const createFor = (
45 src: () => Source,
46 renderItem: (block: any) => Block,
47 getKey?: (item: any, key: any, index?: number) => any,
48 getMemo?: (item: any, key: any, index?: number) => any[],
49 hydrationNode?: Node,
50 once?: boolean,
51): Fragment => {
52 let isMounted = false
53 let oldBlocks: ForBlock[] = []
54 let newBlocks: ForBlock[]
55 let parent: ParentNode | undefined | null
56 const update = getMemo ? updateWithMemo : updateWithoutMemo
57 const parentScope = getCurrentScope()!
58 const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
59 const ref: Fragment = {
60 nodes: oldBlocks,
61 [fragmentKey]: true,
62 }
63
64 const instance = currentInstance!
65 if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) {
66 warn('createFor() can only be used inside setup()')
67 }
68
69 createChildFragmentDirectives(
70 parentAnchor,
71 () => oldBlocks.map(b => b.scope),
72 // source getter
73 () => traverse(src(), 1),
74 // init cb
75 getValue => doFor(getValue()),
76 // effect cb
77 getValue => doFor(getValue()),
78 once,
79 )
80
81 return ref
82
83 function doFor(source: any) {
84 const newLength = getLength(source)
85 const oldLength = oldBlocks.length
86 newBlocks = new Array(newLength)
87
88 if (!isMounted) {
89 isMounted = true
90 mountList(source)
91 } else {
92 parent = parent || parentAnchor.parentNode
93 if (!oldLength) {
94 // fast path for all new
95 mountList(source)
96 } else if (!newLength) {
97 // fast path for clearing
98 for (let i = 0; i < oldLength; i++) {
99 unmount(oldBlocks[i])
100 }
101 } else if (!getKey) {
102 // unkeyed fast path
103 const commonLength = Math.min(newLength, oldLength)
104 for (let i = 0; i < commonLength; i++) {
105 const [item] = getItem(source, i)
106 update((newBlocks[i] = oldBlocks[i]), item)
107 }
108 mountList(source, oldLength)
109 for (let i = newLength; i < oldLength; i++) {
110 unmount(oldBlocks[i])
111 }
112 } else {
113 let i = 0
114 let e1 = oldLength - 1 // prev ending index
115 let e2 = newLength - 1 // next ending index
116
117 // 1. sync from start
118 // (a b) c
119 // (a b) d e
120 while (i <= e1 && i <= e2) {
121 if (tryPatchIndex(source, i)) {
122 i++
123 } else {
124 break
125 }
126 }
127
128 // 2. sync from end
129 // a (b c)
130 // d e (b c)
131 while (i <= e1 && i <= e2) {
132 if (tryPatchIndex(source, i)) {
133 e1--
134 e2--
135 } else {
136 break
137 }
138 }
139
140 // 3. common sequence + mount
141 // (a b)
142 // (a b) c
143 // i = 2, e1 = 1, e2 = 2
144 // (a b)
145 // c (a b)
146 // i = 0, e1 = -1, e2 = 0
147 if (i > e1) {
148 if (i <= e2) {
149 const nextPos = e2 + 1
150 const anchor =
151 nextPos < newLength
152 ? normalizeAnchor(newBlocks[nextPos].nodes)
153 : parentAnchor
154 while (i <= e2) {
155 mount(source, i, anchor)
156 i++
157 }
158 }
159 }
160
161 // 4. common sequence + unmount
162 // (a b) c
163 // (a b)
164 // i = 2, e1 = 2, e2 = 1
165 // a (b c)
166 // (b c)
167 // i = 0, e1 = 0, e2 = -1
168 else if (i > e2) {
169 while (i <= e1) {
170 unmount(oldBlocks[i])
171 i++
172 }
173 }
174
175 // 5. unknown sequence
176 // [i ... e1 + 1]: a b [c d e] f g
177 // [i ... e2 + 1]: a b [e d c h] f g
178 // i = 2, e1 = 4, e2 = 5
179 else {
180 const s1 = i // prev starting index
181 const s2 = i // next starting index
182
183 // 5.1 build key:index map for newChildren
184 const keyToNewIndexMap = new Map()
185 for (i = s2; i <= e2; i++) {
186 keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
187 }
188
189 // 5.2 loop through old children left to be patched and try to patch
190 // matching nodes & remove nodes that are no longer present
191 let j
192 let patched = 0
193 const toBePatched = e2 - s2 + 1
194 let moved = false
195 // used to track whether any node has moved
196 let maxNewIndexSoFar = 0
197 // works as Map<newIndex, oldIndex>
198 // Note that oldIndex is offset by +1
199 // and oldIndex = 0 is a special value indicating the new node has
200 // no corresponding old node.
201 // used for determining longest stable subsequence
202 const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
203
204 for (i = s1; i <= e1; i++) {
205 const prevBlock = oldBlocks[i]
206 if (patched >= toBePatched) {
207 // all new children have been patched so this can only be a removal
208 unmount(prevBlock)
209 } else {
210 const newIndex = keyToNewIndexMap.get(prevBlock.key)
211 if (newIndex == null) {
212 unmount(prevBlock)
213 } else {
214 newIndexToOldIndexMap[newIndex - s2] = i + 1
215 if (newIndex >= maxNewIndexSoFar) {
216 maxNewIndexSoFar = newIndex
217 } else {
218 moved = true
219 }
220 update(
221 (newBlocks[newIndex] = prevBlock),
222 ...getItem(source, newIndex),
223 )
224 patched++
225 }
226 }
227 }
228
229 // 5.3 move and mount
230 // generate longest stable subsequence only when nodes have moved
231 const increasingNewIndexSequence = moved
232 ? getSequence(newIndexToOldIndexMap)
233 : []
234 j = increasingNewIndexSequence.length - 1
235 // looping backwards so that we can use last patched node as anchor
236 for (i = toBePatched - 1; i >= 0; i--) {
237 const nextIndex = s2 + i
238 const anchor =
239 nextIndex + 1 < newLength
240 ? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
241 : parentAnchor
242 if (newIndexToOldIndexMap[i] === 0) {
243 // mount new
244 mount(source, nextIndex, anchor)
245 } else if (moved) {
246 // move if:
247 // There is no stable subsequence (e.g. a reverse)
248 // OR current node is not among the stable sequence
249 if (j < 0 || i !== increasingNewIndexSequence[j]) {
250 insert(newBlocks[nextIndex].nodes, parent!, anchor)
251 } else {
252 j--
253 }
254 }
255 }
256 }
257 }
258 }
259
260 ref.nodes = [(oldBlocks = newBlocks), parentAnchor]
261 }
262
263 function mount(
264 source: any,
265 idx: number,
266 anchor: Node = parentAnchor,
267 ): ForBlock {
268 const scope = new BlockEffectScope(instance, parentScope)
269
270 const [item, key, index] = getItem(source, idx)
271 const state = [
272 shallowRef(item),
273 shallowRef(key),
274 shallowRef(index),
275 ] as ForBlock['state']
276 const block: ForBlock = (newBlocks[idx] = {
277 nodes: null!, // set later
278 scope,
279 state,
280 key: getKey && getKey(item, key, index),
281 memo: getMemo && getMemo(item, key, index),
282 [fragmentKey]: true,
283 })
284 block.nodes = scope.run(() => renderItem(proxyRefs(state)))!
285
286 invokeWithMount(scope, () => {
287 // TODO v-memo
288 // if (getMemo) block.update()
289 if (parent) insert(block.nodes, parent, anchor)
290 })
291
292 return block
293 }
294
295 function mountList(source: any, offset = 0) {
296 for (let i = offset; i < getLength(source); i++) {
297 mount(source, i)
298 }
299 }
300
301 function tryPatchIndex(source: any, idx: number) {
302 const block = oldBlocks[idx]
303 const [item, key, index] = getItem(source, idx)
304 if (block.key === getKey!(item, key, index)) {
305 update((newBlocks[idx] = block), item)
306 return true
307 }
308 }
309
310 function updateWithMemo(
311 block: ForBlock,
312 newItem: any,
313 newKey = block.state[1].value,
314 newIndex = block.state[2].value,
315 ) {
316 const [, key, index] = block.state
317 let needsUpdate = newKey !== key.value || newIndex !== index.value
318 if (!needsUpdate) {
319 const oldMemo = block.memo!
320 const newMemo = (block.memo = getMemo!(newItem, newKey, newIndex))
321 for (let i = 0; i < newMemo.length; i++) {
322 if ((needsUpdate = newMemo[i] !== oldMemo[i])) {
323 break
324 }
325 }
326 }
327
328 if (needsUpdate) setState(block, newItem, newKey, newIndex)
329 invokeWithUpdate(block.scope)
330 }
331
332 function updateWithoutMemo(
333 block: ForBlock,
334 newItem: any,
335 newKey = block.state[1].value,
336 newIndex = block.state[2].value,
337 ) {
338 const [item, key, index] = block.state
339 let needsUpdate =
340 newItem !== item.value ||
341 newKey !== key.value ||
342 newIndex !== index.value ||
343 // shallowRef list
344 (!isReactive(newItem) && isObject(newItem))
345
346 if (needsUpdate) setState(block, newItem, newKey, newIndex)
347 invokeWithUpdate(block.scope)
348 }
349
350 function unmount({ nodes, scope }: ForBlock) {
351 invokeWithUnmount(scope, () => {
352 removeBlock(nodes, parent!)
353 })
354 }
355}Overview
export const createFor = (
src: () => Source,
renderItem: (block: any) => Block,
getKey?: (item: any, key: any, index?: number) => any,
getMemo?: (item: any, key: any, index?: number) => any[],
hydrationNode?: Node,
once?: boolean
): Fragment => {
let oldBlocks: ForBlock[] = [];
// ...
};ForBlock Structure
interface ForBlock extends Fragment {
scope: BlockEffectScope;
state: [
item: ShallowRef<any>,
key: ShallowRef<any>,
index: ShallowRef<number | undefined>
];
key: any;
memo: any[] | undefined;
}Each item is managed as a ForBlock:
scope: Effect scopestate: Array of[item, key, index]asShallowRefkey: The key property valuememo: Cache for v-memo
Diff Algorithm
createFor uses the same Diff algorithm as Vue 3's Virtual DOM:
- Sync from start: Process items with the same key from the beginning
- Sync from end: Process items with the same key from the end
- Common sequence + mount: Add new items
- Common sequence + unmount: Remove old items
- Unknown sequence: Minimal DOM movement using LIS (Longest Increasing Subsequence)
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];mount/unmount
function mount(source: any, idx: number, anchor: Node = parentAnchor): ForBlock {
const scope = new BlockEffectScope(instance, parentScope);
const [item, key, index] = getItem(source, idx);
const state = [shallowRef(item), shallowRef(key), shallowRef(index)];
const block: ForBlock = {
nodes: null!,
scope,
state,
key: getKey && getKey(item, key, index),
// ...
};
block.nodes = scope.run(() => renderItem(proxyRefs(state)))!;
// ...
return block;
}
function unmount({ nodes, scope }: ForBlock) {
invokeWithUnmount(scope, () => {
removeBlock(nodes, parent!);
});
}State Proxying with proxyRefs
The state passed to renderItem is wrapped with proxyRefs.
This allows accessing item, key, and index without using .value in the template.
block.nodes = scope.run(() => renderItem(proxyRefs(state)))!;In the compilation result, they are accessed as _ctx0[0], _ctx0[1], _ctx0[2],
but proxyRefs automatically resolves .value.
v-for in Vapor Mode also adopts the same Diff algorithm as Virtual DOM,
enabling efficient updates using keys.
By combining BlockEffectScope and ShallowRef,
reactive updates and proper lifecycle management are achieved.
