refactor: id rewrite of vapor v-for

This commit is contained in:
三咲智子 Kevin Deng 2024-02-01 17:42:46 +08:00
parent 757af933dc
commit 31f497b1d1
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
9 changed files with 125 additions and 102 deletions

View File

@ -1,29 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-for > basic v-for 1`] = `
"import { template as _template, fragment as _fragment, children as _children, on as _on, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, append as _append } from 'vue/vapor';
"import { template as _template, fragment as _fragment, renderEffect as _renderEffect, children as _children, on as _on, setText as _setText, createFor as _createFor, append as _append } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createFor(() => (_ctx.items), (_block) => {
let item
_renderEffect(() => {
([item] = _block.s);
})
const n2 = t0()
const { 0: [n3],} = _children(n2)
_on(n3, "click", $event => (_ctx.remove(_block.s[0])))
const _updateEffect = () => {
const [item] = _block.s
_on(n3, "click", $event => (_ctx.remove(item)))
_renderEffect(() => {
_setText(n3, item)
}
_renderEffect(_updateEffect)
return [n2, _updateEffect]
})
return n2
})
_append(n0, n1)
return n0
}"
`;
exports[`compiler: v-for > basic v-for 2`] = `
exports[`compiler: v-for > no value 1`] = `
"import { template as _template, fragment as _fragment, createFor as _createFor, append as _append } from 'vue/vapor';
export function render(_ctx) {
@ -32,7 +34,37 @@ export function render(_ctx) {
const n0 = t1()
const n1 = _createFor(() => (_ctx.items), (_block) => {
const n2 = t0()
return [n2, () => {}]
return n2
})
_append(n0, n1)
return n0
}"
`;
exports[`compiler: v-for > object de-structured value 1`] = `
"import { template as _template, fragment as _fragment, renderEffect as _renderEffect, children as _children, createTextNode as _createTextNode, append as _append, setText as _setText, createFor as _createFor } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<span></span>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createFor(() => (_ctx.items), (_block) => {
let id, value
_renderEffect(() => {
([{ id, value }] = _block.s);
})
const n2 = t0()
const { 0: [n5],} = _children(n2)
const n3 = _createTextNode()
const n4 = _createTextNode()
_append(n5, n3, n4)
_renderEffect(() => {
_setText(n3, id)
})
_renderEffect(() => {
_setText(n4, value)
})
return n2
})
_append(n0, n1)
return n0

View File

@ -66,8 +66,15 @@ describe('compiler: v-for', () => {
expect((ir.operation[0] as ForIRNode).render.effect).lengthOf(1)
})
test('basic v-for', () => {
test('no value', () => {
const { code } = compileWithVFor(`<div v-for=" of items">item</div>`)
expect(code).matchSnapshot()
})
test('object de-structured value', () => {
const { code } = compileWithVFor(
'<span v-for="({ id, value }) in items">{{ id }}{{ value }}</span>',
)
expect(code).matchSnapshot()
})
})

View File

@ -7,9 +7,9 @@ import {
advancePositionWithMutation,
locStub,
} from '@vue/compiler-dom'
import type { IREffect, RootIRNode, VaporHelper } from './ir'
import type { RootIRNode, VaporHelper } from './ir'
import { SourceMapGenerator } from 'source-map-js'
import { extend, isString, remove } from '@vue/shared'
import { extend, isString } from '@vue/shared'
import type { ParserPlugin } from '@babel/parser'
import { genTemplate } from './generators/template'
import { genBlockFunctionContent } from './generators/block'
@ -69,22 +69,19 @@ export class CodegenContext {
return `_${name}`
}
identifiers: Record<string, string[]> = Object.create(null)
withId = <T>(fn: () => T, map: Record<string, string | null>): T => {
identifiers: Record<string, number> = Object.create(null)
withId = <T>(fn: () => T, ids: string[]): T => {
const { identifiers } = this
const ids = Object.keys(map)
for (const id of ids) {
identifiers[id] ||= []
identifiers[id].unshift(map[id] || id)
if (identifiers[id] === undefined) identifiers[id] = 0
identifiers[id]!++
}
const ret = fn()
ids.forEach(id => remove(identifiers[id], map[id] || id))
ids.forEach(id => identifiers[id]!--)
return ret
}
genEffect?: (effects: IREffect[]) => CodeFragment[]
constructor(
public ir: RootIRNode,

View File

@ -21,13 +21,15 @@ export function genBlockFunction(
oper: BlockFunctionIRNode,
context: CodegenContext,
args: CodeFragment[] = [],
returnValue?: () => CodeFragment[],
preamble: CodeFragment[] = [],
returnValue?: CodeFragment[],
): CodeFragment[] {
return [
'(',
...args,
') => {',
INDENT_START,
...preamble,
...genBlockFunctionContent(oper, context, returnValue),
INDENT_END,
NEWLINE,
@ -38,7 +40,7 @@ export function genBlockFunction(
export function genBlockFunctionContent(
ir: BlockFunctionIRNode | RootIRNode,
context: CodegenContext,
returnValue?: () => CodeFragment[],
returnValue?: CodeFragment[],
): CodeFragment[] {
const { vaporHelper } = context
const [frag, push] = buildCodeFragment(
@ -65,11 +67,7 @@ export function genBlockFunctionContent(
push(...genOperations(ir.operation, context))
push(...genEffects(ir.effect, context))
push(
NEWLINE,
'return ',
...(returnValue ? returnValue() : [`n${ir.dynamic.id}`]),
)
push(NEWLINE, 'return ', ...(returnValue || [`n${ir.dynamic.id}`]))
return frag
}

View File

@ -45,9 +45,10 @@ export function genSetEvent(
const hasMultipleStatements = exp.content.includes(`;`)
if (isInlineStatement) {
const expr = context.withId(() => genExpression(exp, context), {
$event: null,
})
const expr = context.withId(
() => genExpression(exp, context),
['$event'],
)
return [
'$event => ',
hasMultipleStatements ? '{' : '(',

View File

@ -88,9 +88,8 @@ function genIdentifier(
const { inline, bindingMetadata } = options
let name: string | undefined = id
const idMap = identifiers[id]
if (idMap && idMap.length) {
return [idMap[0], NewlineType.None, loc]
if (identifiers[id]) {
return [id, NewlineType.None, loc]
}
if (inline) {

View File

@ -6,11 +6,14 @@ import {
INDENT_END,
INDENT_START,
NEWLINE,
buildCodeFragment,
} from '../generate'
import type { ForIRNode, IREffect } from '../ir'
import { genOperations } from './operation'
import { NewlineType } from '@vue/compiler-dom'
import type { ForIRNode } from '../ir'
import {
NewlineType,
type SimpleExpressionNode,
walkIdentifiers,
} from '@vue/compiler-dom'
import type { ArrowFunctionExpression } from '@babel/types'
export function genFor(
oper: ForIRNode,
@ -19,73 +22,63 @@ export function genFor(
const { call, vaporHelper } = context
const { source, value, key, render } = oper
const rawValue = value && value.content
const rawKey = key && key.content
const sourceExpr = ['() => (', ...genExpression(source, context), ')']
let updateFn = '_updateEffect'
context.genEffect = genEffectInFor
const valueIds = value ? extractParams(value) : new Set<string>()
const keyIds = key ? extractParams(key) : new Set<string>()
const ids = [...valueIds, ...keyIds]
const idMap: Record<string, string> = {}
if (rawValue) idMap[rawValue] = `_block.s[0]`
if (rawKey) idMap[rawKey] = `_block.s[1]`
let preamble: CodeFragment[] = []
if (value || rawKey) {
const assignment: CodeFragment[] = ['let ', ids.join(', ')]
const blockRet = (): CodeFragment[] => [
`[n${render.dynamic.id!}, ${updateFn}]`,
]
preamble = [
NEWLINE,
...assignment,
NEWLINE,
...call(vaporHelper('renderEffect'), [
'() => {',
INDENT_START,
NEWLINE,
'(',
'[',
value && [value.content, NewlineType.None, value.loc],
rawKey && ', ',
rawKey && [rawKey, NewlineType.None, key.loc],
'] = _block.s',
');',
INDENT_END,
NEWLINE,
'}',
]),
]
}
const blockRet: CodeFragment[] = [`n${render.dynamic.id!}`]
const blockFn = context.withId(
() => genBlockFunction(render, context, ['_block'], blockRet),
idMap,
() => genBlockFunction(render, context, ['_block'], preamble, blockRet),
ids,
)
context.genEffect = undefined
return [
NEWLINE,
`const n${oper.id} = `,
...call(vaporHelper('createFor'), sourceExpr, blockFn),
]
function genEffectInFor(effects: IREffect[]): CodeFragment[] {
if (!effects.length) {
updateFn = '() => {}'
return []
}
const [frag, push] = buildCodeFragment(INDENT_START)
// const [value, key] = _block.s
if (rawValue || rawKey) {
push(
NEWLINE,
'const ',
'[',
rawValue && [rawValue, NewlineType.None, value.loc],
rawKey && ', ',
rawKey && [rawKey, NewlineType.None, key.loc],
'] = _block.s',
)
}
const idMap: Record<string, string | null> = {}
if (value) idMap[value.content] = null
if (key) idMap[key.content] = null
context.withId(() => {
effects.forEach(effect =>
push(...genOperations(effect.operations, context)),
)
}, idMap)
push(INDENT_END)
return [
NEWLINE,
`const ${updateFn} = () => {`,
...frag,
NEWLINE,
'}',
NEWLINE,
`${vaporHelper('renderEffect')}(${updateFn})`,
]
}
}
function extractParams(node: SimpleExpressionNode) {
const ids = new Set<string>()
if (node.ast === null || node.ast === false) {
ids.add(node.content)
} else {
walkIdentifiers(
node.ast as ArrowFunctionExpression,
(id, parent, parentStack, isReference, isLocal) => {
if (isLocal) ids.add(id.name)
},
true,
)
}
return ids
}

View File

@ -25,7 +25,7 @@ export function genOperations(opers: OperationNode[], context: CodegenContext) {
return frag
}
function genOperation(
export function genOperation(
oper: OperationNode,
context: CodegenContext,
): CodeFragment[] {
@ -60,9 +60,6 @@ function genOperation(
}
export function genEffects(effects: IREffect[], context: CodegenContext) {
if (context.genEffect) {
return context.genEffect(effects)
}
const [frag, push] = buildCodeFragment()
for (const effect of effects) {
push(...genEffect(effect, context))
@ -70,7 +67,7 @@ export function genEffects(effects: IREffect[], context: CodegenContext) {
return frag
}
function genEffect({ operations }: IREffect, context: CodegenContext) {
export function genEffect({ operations }: IREffect, context: CodegenContext) {
const { vaporHelper } = context
const [frag, push] = buildCodeFragment(
NEWLINE,

View File

@ -15,7 +15,7 @@ interface ForBlock extends Fragment {
export const createFor = (
src: () => any[] | Record<string, string> | Set<any> | Map<any, any>,
renderItem: (block: ForBlock) => [Block, () => void],
renderItem: (block: ForBlock) => Block,
getKey: ((item: any, index: number) => any) | null,
getMemo?: (item: any) => any[],
hydrationNode?: Node,
@ -47,9 +47,8 @@ export const createFor = (
memo: getMemo && getMemo(item),
[fragmentKey]: true,
})
const res = scope.run(() => renderItem(block))!
block.nodes = res[0]
block.update = res[1]
block.nodes = scope.run(() => renderItem(block))!
block.update = () => scope.effects.forEach(effect => effect.run())
if (getMemo) block.update()
if (parent) insert(block.nodes, parent, anchor)
return block