wip: v-for destructure expression rewrite (part 1)

This commit is contained in:
Evan You 2025-01-30 20:06:41 +08:00
parent e49c5a17da
commit fca1aef896
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
6 changed files with 173 additions and 125 deletions

View File

@ -1,15 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-for > array de-structured value 1`] = `
"import { setText as _setText, renderEffect as _renderEffect, withDestructure as _withDestructure, createFor as _createFor, template as _template } from 'vue';
"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), _withDestructure(([[id, ...other], index]) => [id, other, index], (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]))
_renderEffect(() => _setText(n2, _ctx0[0].value[0] + _ctx0[0].value[1] + _ctx0[1].value))
return n2
}), ([id, ...other], index) => (id))
}, ([id, other], index) => (id))
return n0
}"
`;
@ -73,7 +73,7 @@ export function render(_ctx) {
const n4 = t0()
_renderEffect(() => _setText(n4, _ctx1[0].value+_ctx0[0].value))
return n4
}, null, n5)
})
_insert(n2, n5)
return n5
})
@ -81,16 +81,44 @@ export function render(_ctx) {
}"
`;
exports[`compiler: v-for > object de-structured value 1`] = `
"import { setText as _setText, renderEffect as _renderEffect, withDestructure as _withDestructure, createFor as _createFor, template as _template } from 'vue';
exports[`compiler: v-for > object de-structured value (with rest) 1`] = `
"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), _withDestructure(([{ id, ...other }, index]) => [id, other, index], (_ctx0) => {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]))
_renderEffect(() => _setText(n2, _ctx0[0].value.id + _ctx0[0].value + _ctx0[1].value))
return n2
}), ({ id, ...other }, index) => (id))
}, ({ id, ...other }, index) => (id))
return n0
}"
`;
exports[`compiler: v-for > object de-structured value 1`] = `
"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<span></span>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0].value.id, _ctx0[0].value.value))
return n2
}, ({ id, value }) => (id))
return n0
}"
`;
exports[`compiler: v-for > object de-structured value 2`] = `
"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
const t0 = _template("<div></div>", true)
export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n2 = t0()
_renderEffect(() => _setText(n2, _ctx0[0].value.id + _ctx0[0].value + _ctx0[1].value))
return n2
}, ({ id, ...other }, index) => (id))
return n0
}"
`;

View File

@ -68,7 +68,7 @@ export function render(_ctx) {
const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
const n2 = t0()
return n2
}, null, null, null, true)
}, null, null, true)
return n0
}"
`;

View File

@ -79,13 +79,6 @@ describe('compiler: v-for', () => {
expect(code).matchSnapshot()
})
test.todo('object de-structured value', () => {
const { code } = compileWithVFor(
'<span v-for="({ id, value }) in items">{{ id }}{{ value }}</span>',
)
expect(code).matchSnapshot()
})
test('nested v-for', () => {
const { code, ir } = compileWithVFor(
`<div v-for="i in list"><span v-for="j in i">{{ j+i }}</span></div>`,
@ -124,12 +117,38 @@ describe('compiler: v-for', () => {
})
test('object de-structured value', () => {
const { code, ir } = compileWithVFor(
'<span v-for="({ id, value }) in items" :key="id">{{ id }}{{ value }}</span>',
)
expect(code).matchSnapshot()
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,
source: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'items',
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: '{ id, value }',
ast: {
type: 'ArrowFunctionExpression',
params: [
{
type: 'ObjectPattern',
},
],
},
},
key: undefined,
index: undefined,
})
})
test.todo('object de-structured value (with rest)', () => {
const { code, ir } = compileWithVFor(
`<div v-for="( { id, ...other }, index) in list" :key="id">{{ id + other + index }}</div>`,
)
expect(code).matchSnapshot()
expect(code).contains(`([{ id, ...other }, index]) => [id, other, index]`)
expect(code).contains(`_ctx0[0] + _ctx0[1] + _ctx0[2]`)
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,
source: {
@ -157,12 +176,41 @@ describe('compiler: v-for', () => {
})
test('array de-structured value', () => {
const { code, ir } = compileWithVFor(
`<div v-for="([id, other], index) in list" :key="id">{{ id + other + index }}</div>`,
)
expect(code).matchSnapshot()
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,
source: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'list',
},
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: '[id, other]',
ast: {
type: 'ArrowFunctionExpression',
params: [
{
type: 'ArrayPattern',
},
],
},
},
key: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: 'index',
},
index: undefined,
})
})
test.todo('array de-structured value (with rest)', () => {
const { code, ir } = compileWithVFor(
`<div v-for="([id, ...other], index) in list" :key="id">{{ id + other + index }}</div>`,
)
expect(code).matchSnapshot()
expect(code).contains(`([[id, ...other], index]) => [id, other, index]`)
expect(code).contains(`_ctx0[0] + _ctx0[1] + _ctx0[2]`)
expect(ir.block.operation[0]).toMatchObject({
type: IRNodeTypes.FOR,
source: {
@ -189,7 +237,7 @@ describe('compiler: v-for', () => {
})
})
test('v-for aliases w/ complex expressions', () => {
test.todo('v-for aliases w/ complex expressions', () => {
const { code, ir } = compileWithVFor(
`<div v-for="({ foo = bar, baz: [qux = quux] }) in list">
{{ foo + bar + baz + qux + quux }}
@ -222,17 +270,4 @@ describe('compiler: v-for', () => {
index: undefined,
})
})
test('function params w/ prefixIdentifiers: false', () => {
const { code } = compileWithVFor(
`<div v-for="(item, , k) of items" :key="k">{{ item }}</div>`,
{
prefixIdentifiers: false,
},
)
expect(code).contains(`_createFor(() => (items), ([item, __, k]) => {`)
expect(code).contain(`_setText(n2, item)`)
expect(code).matchSnapshot()
})
})

View File

@ -3,56 +3,39 @@ import { genBlock } from './block'
import { genExpression } from './expression'
import type { CodegenContext } from '../generate'
import type { ForIRNode } from '../ir'
import {
type CodeFragment,
DELIMITERS_ARRAY,
NEWLINE,
genCall,
genMulti,
} from './utils'
import { type CodeFragment, NEWLINE, genCall, genMulti } from './utils'
import type { Identifier } from '@babel/types'
export function genFor(
oper: ForIRNode,
context: CodegenContext,
): CodeFragment[] {
const { helper } = context
const { source, value, key, index, render, keyProp, once, id, container } =
const { source, value, key, index, render, keyProp, once, id, component } =
oper
let isDestructure = false
let rawValue: string | null = null
const rawKey = key && key.content
const rawIndex = index && index.content
const sourceExpr = ['() => (', ...genExpression(source, context), ')']
const idsInValue = getIdsInValue()
let blockFn = genBlockFn()
const simpleIdMap: Record<string, null> = genSimpleIdMap()
const idToPathMap = parseValueDestructure()
if (isDestructure) {
const idMap: Record<string, null> = {}
idsInValue.forEach(id => (idMap[id] = null))
if (rawKey) idMap[rawKey] = null
if (rawIndex) idMap[rawIndex] = null
const destructureAssignmentFn: CodeFragment[] = [
'(',
...genMulti(
DELIMITERS_ARRAY,
rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
rawKey ? rawKey : rawIndex ? '__' : undefined,
rawIndex,
),
') => ',
...genMulti(DELIMITERS_ARRAY, ...idsInValue, rawKey, rawIndex),
]
const [depth, exitScope] = context.enterScope()
const propsName = `_ctx${depth}`
const idMap: Record<string, string | null> = {}
blockFn = genCall(
// @ts-expect-error
helper('withDestructure'),
destructureAssignmentFn,
blockFn,
)
}
idToPathMap.forEach((path, id) => {
idMap[id] = `${propsName}[0].value${path}`
})
if (rawKey) idMap[rawKey] = `${propsName}[1].value`
if (rawIndex) idMap[rawIndex] = `${propsName}[2].value`
const blockFn = context.withId(
() => genBlock(render, context, [propsName]),
idMap,
)
exitScope()
return [
NEWLINE,
@ -62,67 +45,68 @@ export function genFor(
sourceExpr,
blockFn,
genCallback(keyProp),
container != null && `n${container}`,
false, // todo: hydrationNode
component && 'true',
once && 'true',
// todo: hydrationNode
),
]
function getIdsInValue() {
const idsInValue = new Set<string>()
// construct a id -> accessor path map.
// e.g. `{ x: { y: [z] }}` -> `Map{ 'z' => '.x.y[0]' }`
function parseValueDestructure() {
const map = new Map<string, string>()
if (value) {
rawValue = value && value.content
if ((isDestructure = !!value.ast)) {
if (value.ast) {
walkIdentifiers(
value.ast,
(id, _, __, ___, isLocal) => {
if (isLocal) idsInValue.add(id.name)
(id, _, parentStack, ___, isLocal) => {
if (isLocal) {
let path = ''
for (let i = 0; i < parentStack.length; i++) {
const parent = parentStack[i]
const child = parentStack[i + 1] || id
if (
parent.type === 'ObjectProperty' &&
parent.value === child
) {
if (parent.computed && parent.key.type !== 'StringLiteral') {
// TODO need to process this
path += `[${value.content.slice(
parent.key.start!,
parent.key.end!,
)}]`
} else if (parent.key.type === 'StringLiteral') {
path += `[${JSON.stringify(parent.key.value)}]`
} else {
// non-computed, can only be identifier
path += `.${(parent.key as Identifier).name}`
}
} else if (parent.type === 'ArrayPattern') {
const index = parent.elements.indexOf(child as any)
path += `[${index}]`
}
// TODO handle rest spread
}
map.set(id.name, path)
}
},
true,
)
} else {
idsInValue.add(rawValue)
map.set(rawValue, '')
}
}
return idsInValue
}
function genBlockFn() {
const [depth, exitScope] = context.enterScope()
let propsName: string
const idMap: Record<string, string | null> = {}
if (context.options.prefixIdentifiers) {
propsName = `_ctx${depth}`
let suffix = isDestructure ? '' : '.value'
Array.from(idsInValue).forEach(
(id, idIndex) => (idMap[id] = `${propsName}[${idIndex}]${suffix}`),
)
if (rawKey) idMap[rawKey] = `${propsName}[${idsInValue.size}]${suffix}`
if (rawIndex)
idMap[rawIndex] = `${propsName}[${idsInValue.size + 1}]${suffix}`
} else {
propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
}
const blockFn = context.withId(
() => genBlock(render, context, [propsName]),
idMap,
)
exitScope()
return blockFn
}
function genSimpleIdMap() {
const idMap: Record<string, null> = {}
if (rawKey) idMap[rawKey] = null
if (rawIndex) idMap[rawIndex] = null
idsInValue.forEach(id => (idMap[id] = null))
return idMap
return map
}
// TODO this should be looked at for destructure cases
function genCallback(expr: SimpleExpressionNode | undefined) {
if (!expr) return false
const res = context.withId(() => genExpression(expr, context), simpleIdMap)
const res = context.withId(
() => genExpression(expr, context),
genSimpleIdMap(),
)
return [
...genMulti(
['(', ')', ', '],
@ -135,4 +119,12 @@ export function genFor(
')',
]
}
function genSimpleIdMap() {
const idMap: Record<string, null> = {}
if (rawKey) idMap[rawKey] = null
if (rawIndex) idMap[rawIndex] = null
idToPathMap.forEach((_, id) => (idMap[id] = null))
return idMap
}
}

View File

@ -90,7 +90,7 @@ export interface ForIRNode extends BaseIRNode, IRFor {
keyProp?: SimpleExpressionNode
render: BlockIRNode
once: boolean
container?: number
component: boolean
}
export interface SetPropIRNode extends BaseIRNode {

View File

@ -1,5 +1,6 @@
import {
type ElementNode,
ElementTypes,
ErrorCodes,
type SimpleExpressionNode,
createCompilerError,
@ -46,6 +47,7 @@ export function processFor(
const keyProp = findProp(node, 'key')
const keyProperty = keyProp && propToExpression(keyProp)
const isComponent = node.tagType === ElementTypes.COMPONENT
context.node = node = wrapTemplate(node, ['for'])
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
const id = context.reference()
@ -55,15 +57,6 @@ export function processFor(
return (): void => {
exitBlock()
const { parent } = context
let container: number | undefined
if (
parent &&
parent.block.node !== parent.node &&
parent.node.children.length === 1
) {
container = parent.reference()
}
context.registerOperation({
type: IRNodeTypes.FOR,
id,
@ -74,7 +67,7 @@ export function processFor(
keyProp: keyProperty,
render,
once: context.inVOnce,
container,
component: isComponent,
})
}
}