feat(compiler-vapor): v-for (#101)
This commit is contained in:
parent
7b036fd4c0
commit
681dc5d954
|
@ -0,0 +1,40 @@
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
export function render(_ctx) {
|
||||||
|
const t0 = _template("<div></div>")
|
||||||
|
const t1 = _fragment()
|
||||||
|
const n0 = t1()
|
||||||
|
const n1 = _createFor(() => (_ctx.items), (_block) => {
|
||||||
|
const n2 = t0()
|
||||||
|
const { 0: [n3],} = _children(n2)
|
||||||
|
_on(n3, "click", $event => (_ctx.remove(_block.s[0])))
|
||||||
|
const _updateEffect = () => {
|
||||||
|
const [item] = _block.s
|
||||||
|
_setText(n3, item)
|
||||||
|
}
|
||||||
|
_renderEffect(_updateEffect)
|
||||||
|
return [n2, _updateEffect]
|
||||||
|
})
|
||||||
|
_append(n0, n1)
|
||||||
|
return n0
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`compiler: v-for > basic v-for 2`] = `
|
||||||
|
"import { template as _template, fragment as _fragment, createFor as _createFor, append as _append } from 'vue/vapor';
|
||||||
|
|
||||||
|
export function render(_ctx) {
|
||||||
|
const t0 = _template("<div>item</div>")
|
||||||
|
const t1 = _fragment()
|
||||||
|
const n0 = t1()
|
||||||
|
const n1 = _createFor(() => (_ctx.items), (_block) => {
|
||||||
|
const n2 = t0()
|
||||||
|
return [n2, () => {}]
|
||||||
|
})
|
||||||
|
_append(n0, n1)
|
||||||
|
return n0
|
||||||
|
}"
|
||||||
|
`;
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { makeCompile } from './_utils'
|
||||||
|
import {
|
||||||
|
type ForIRNode,
|
||||||
|
IRNodeTypes,
|
||||||
|
transformElement,
|
||||||
|
transformInterpolation,
|
||||||
|
transformVFor,
|
||||||
|
transformVOn,
|
||||||
|
} from '../../src'
|
||||||
|
import { NodeTypes } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
const compileWithVFor = makeCompile({
|
||||||
|
nodeTransforms: [transformInterpolation, transformVFor, transformElement],
|
||||||
|
directiveTransforms: { on: transformVOn },
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('compiler: v-for', () => {
|
||||||
|
test('basic v-for', () => {
|
||||||
|
const { code, ir, vaporHelpers, helpers } = compileWithVFor(
|
||||||
|
`<div v-for="item of items" @click="remove(item)">{{ item }}</div>`,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(code).matchSnapshot()
|
||||||
|
expect(vaporHelpers).contains('createFor')
|
||||||
|
expect(helpers.size).toBe(0)
|
||||||
|
expect(ir.template).lengthOf(2)
|
||||||
|
expect(ir.template).toMatchObject([
|
||||||
|
{
|
||||||
|
template: '<div></div>',
|
||||||
|
type: IRNodeTypes.TEMPLATE_FACTORY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: IRNodeTypes.FRAGMENT_FACTORY,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(ir.operation).toMatchObject([
|
||||||
|
{
|
||||||
|
type: IRNodeTypes.FOR,
|
||||||
|
id: 1,
|
||||||
|
source: {
|
||||||
|
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||||
|
content: 'items',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||||
|
content: 'item',
|
||||||
|
},
|
||||||
|
key: undefined,
|
||||||
|
index: undefined,
|
||||||
|
render: {
|
||||||
|
type: IRNodeTypes.BLOCK_FUNCTION,
|
||||||
|
templateIndex: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: IRNodeTypes.APPEND_NODE,
|
||||||
|
elements: [1],
|
||||||
|
parent: 0,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
expect(ir.dynamic).toMatchObject({
|
||||||
|
id: 0,
|
||||||
|
children: { 0: { id: 1 } },
|
||||||
|
})
|
||||||
|
expect(ir.effect).toEqual([])
|
||||||
|
expect((ir.operation[0] as ForIRNode).render.effect).lengthOf(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('basic v-for', () => {
|
||||||
|
const { code } = compileWithVFor(`<div v-for=" of items">item</div>`)
|
||||||
|
expect(code).matchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -25,6 +25,7 @@ import { transformInterpolation } from './transforms/transformInterpolation'
|
||||||
import type { HackOptions } from './ir'
|
import type { HackOptions } from './ir'
|
||||||
import { transformVModel } from './transforms/vModel'
|
import { transformVModel } from './transforms/vModel'
|
||||||
import { transformVIf } from './transforms/vIf'
|
import { transformVIf } from './transforms/vIf'
|
||||||
|
import { transformVFor } from './transforms/vFor'
|
||||||
|
|
||||||
export type CompilerOptions = HackOptions<BaseCompilerOptions>
|
export type CompilerOptions = HackOptions<BaseCompilerOptions>
|
||||||
|
|
||||||
|
@ -102,6 +103,7 @@ export function getBaseTransformPreset(
|
||||||
transformRef,
|
transformRef,
|
||||||
transformInterpolation,
|
transformInterpolation,
|
||||||
transformVIf,
|
transformVIf,
|
||||||
|
transformVFor,
|
||||||
transformElement,
|
transformElement,
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,23 +7,10 @@ import {
|
||||||
advancePositionWithMutation,
|
advancePositionWithMutation,
|
||||||
locStub,
|
locStub,
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
import {
|
import type { IREffect, RootIRNode, VaporHelper } from './ir'
|
||||||
IRNodeTypes,
|
|
||||||
type OperationNode,
|
|
||||||
type RootIRNode,
|
|
||||||
type VaporHelper,
|
|
||||||
} from './ir'
|
|
||||||
import { SourceMapGenerator } from 'source-map-js'
|
import { SourceMapGenerator } from 'source-map-js'
|
||||||
import { extend, isString } from '@vue/shared'
|
import { extend, isString, remove } from '@vue/shared'
|
||||||
import type { ParserPlugin } from '@babel/parser'
|
import type { ParserPlugin } from '@babel/parser'
|
||||||
import { genSetProp } from './generators/prop'
|
|
||||||
import { genCreateTextNode, genSetText } from './generators/text'
|
|
||||||
import { genSetEvent } from './generators/event'
|
|
||||||
import { genSetHtml } from './generators/html'
|
|
||||||
import { genSetRef } from './generators/ref'
|
|
||||||
import { genSetModelValue } from './generators/modelValue'
|
|
||||||
import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom'
|
|
||||||
import { genIf } from './generators/if'
|
|
||||||
import { genTemplate } from './generators/template'
|
import { genTemplate } from './generators/template'
|
||||||
import { genBlockFunctionContent } from './generators/block'
|
import { genBlockFunctionContent } from './generators/block'
|
||||||
|
|
||||||
|
@ -89,6 +76,23 @@ export class CodegenContext {
|
||||||
return `_${name}`
|
return `_${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
identifiers: Record<string, string[]> = Object.create(null)
|
||||||
|
withId = <T>(fn: () => T, map: Record<string, string | null>): T => {
|
||||||
|
const { identifiers } = this
|
||||||
|
const ids = Object.keys(map)
|
||||||
|
|
||||||
|
for (const id of ids) {
|
||||||
|
identifiers[id] ||= []
|
||||||
|
identifiers[id].unshift(map[id] || id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = fn()
|
||||||
|
ids.forEach(id => remove(identifiers[id], map[id] || id))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
genEffect?: (effects: IREffect[]) => CodeFragment[]
|
||||||
|
|
||||||
constructor(ir: RootIRNode, options: CodegenOptions) {
|
constructor(ir: RootIRNode, options: CodegenOptions) {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
mode: 'function',
|
mode: 'function',
|
||||||
|
@ -270,45 +274,3 @@ export function buildCodeFragment() {
|
||||||
const push = frag.push.bind(frag)
|
const push = frag.push.bind(frag)
|
||||||
return [frag, push] as const
|
return [frag, push] as const
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genOperation(
|
|
||||||
oper: OperationNode,
|
|
||||||
context: CodegenContext,
|
|
||||||
): CodeFragment[] {
|
|
||||||
// TODO: cache old value
|
|
||||||
switch (oper.type) {
|
|
||||||
case IRNodeTypes.SET_PROP:
|
|
||||||
return genSetProp(oper, context)
|
|
||||||
case IRNodeTypes.SET_TEXT:
|
|
||||||
return genSetText(oper, context)
|
|
||||||
case IRNodeTypes.SET_EVENT:
|
|
||||||
return genSetEvent(oper, context)
|
|
||||||
case IRNodeTypes.SET_HTML:
|
|
||||||
return genSetHtml(oper, context)
|
|
||||||
case IRNodeTypes.SET_REF:
|
|
||||||
return genSetRef(oper, context)
|
|
||||||
case IRNodeTypes.SET_MODEL_VALUE:
|
|
||||||
return genSetModelValue(oper, context)
|
|
||||||
case IRNodeTypes.CREATE_TEXT_NODE:
|
|
||||||
return genCreateTextNode(oper, context)
|
|
||||||
case IRNodeTypes.INSERT_NODE:
|
|
||||||
return genInsertNode(oper, context)
|
|
||||||
case IRNodeTypes.PREPEND_NODE:
|
|
||||||
return genPrependNode(oper, context)
|
|
||||||
case IRNodeTypes.APPEND_NODE:
|
|
||||||
return genAppendNode(oper, context)
|
|
||||||
case IRNodeTypes.IF:
|
|
||||||
return genIf(oper, context)
|
|
||||||
case IRNodeTypes.WITH_DIRECTIVE:
|
|
||||||
// generated, skip
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return checkNever(oper)
|
|
||||||
}
|
|
||||||
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove when stable
|
|
||||||
// @ts-expect-error
|
|
||||||
function checkNever(x: never): never {}
|
|
||||||
|
|
|
@ -10,18 +10,22 @@ import {
|
||||||
type CodeFragment,
|
type CodeFragment,
|
||||||
type CodegenContext,
|
type CodegenContext,
|
||||||
buildCodeFragment,
|
buildCodeFragment,
|
||||||
genOperation,
|
|
||||||
} from '../generate'
|
} from '../generate'
|
||||||
import { genWithDirective } from './directive'
|
import { genWithDirective } from './directive'
|
||||||
|
import { genEffects, genOperations } from './operation'
|
||||||
|
|
||||||
export function genBlockFunction(
|
export function genBlockFunction(
|
||||||
oper: BlockFunctionIRNode,
|
oper: BlockFunctionIRNode,
|
||||||
context: CodegenContext,
|
context: CodegenContext,
|
||||||
|
args: CodeFragment[] = [],
|
||||||
|
returnValue?: () => CodeFragment[],
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const { newline, withIndent } = context
|
const { newline, withIndent } = context
|
||||||
return [
|
return [
|
||||||
'() => {',
|
'(',
|
||||||
...withIndent(() => genBlockFunctionContent(oper, context)),
|
...args,
|
||||||
|
') => {',
|
||||||
|
...withIndent(() => genBlockFunctionContent(oper, context, returnValue)),
|
||||||
newline(),
|
newline(),
|
||||||
'}',
|
'}',
|
||||||
]
|
]
|
||||||
|
@ -30,8 +34,9 @@ export function genBlockFunction(
|
||||||
export function genBlockFunctionContent(
|
export function genBlockFunctionContent(
|
||||||
ir: BlockFunctionIRNode | RootIRNode,
|
ir: BlockFunctionIRNode | RootIRNode,
|
||||||
ctx: CodegenContext,
|
ctx: CodegenContext,
|
||||||
|
returnValue?: () => CodeFragment[],
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const { newline, withIndent, vaporHelper } = ctx
|
const { newline, vaporHelper } = ctx
|
||||||
const [frag, push] = buildCodeFragment()
|
const [frag, push] = buildCodeFragment()
|
||||||
|
|
||||||
push(newline(), `const n${ir.dynamic.id} = t${ir.templateIndex}()`)
|
push(newline(), `const n${ir.dynamic.id} = t${ir.templateIndex}()`)
|
||||||
|
@ -52,19 +57,14 @@ export function genBlockFunctionContent(
|
||||||
push(...genWithDirective(directives, ctx))
|
push(...genWithDirective(directives, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const operation of ir.operation) {
|
push(...genOperations(ir.operation, ctx))
|
||||||
push(...genOperation(operation, ctx))
|
push(...genEffects(ir.effect, ctx))
|
||||||
}
|
|
||||||
|
|
||||||
for (const { operations } of ir.effect) {
|
push(
|
||||||
push(newline(), `${vaporHelper('renderEffect')}(() => {`)
|
newline(),
|
||||||
withIndent(() => {
|
'return ',
|
||||||
operations.forEach(op => push(...genOperation(op, ctx)))
|
...(returnValue ? returnValue() : [`n${ir.dynamic.id}`]),
|
||||||
})
|
)
|
||||||
push(newline(), '})')
|
|
||||||
}
|
|
||||||
|
|
||||||
push(newline(), `return n${ir.dynamic.id}`)
|
|
||||||
|
|
||||||
return frag
|
return frag
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,13 +45,13 @@ export function genSetEvent(
|
||||||
const hasMultipleStatements = exp.content.includes(`;`)
|
const hasMultipleStatements = exp.content.includes(`;`)
|
||||||
|
|
||||||
if (isInlineStatement) {
|
if (isInlineStatement) {
|
||||||
const knownIds = Object.create(null)
|
const expr = context.withId(() => genExpression(exp, context), {
|
||||||
knownIds['$event'] = 1
|
$event: null,
|
||||||
|
})
|
||||||
return [
|
return [
|
||||||
'$event => ',
|
'$event => ',
|
||||||
hasMultipleStatements ? '{' : '(',
|
hasMultipleStatements ? '{' : '(',
|
||||||
...genExpression(exp, context, knownIds),
|
...expr,
|
||||||
hasMultipleStatements ? '}' : ')',
|
hasMultipleStatements ? '}' : ')',
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
export function genExpression(
|
export function genExpression(
|
||||||
node: IRExpression,
|
node: IRExpression,
|
||||||
context: CodegenContext,
|
context: CodegenContext,
|
||||||
knownIds: Record<string, number> = Object.create(null),
|
|
||||||
): CodeFragment[] {
|
): CodeFragment[] {
|
||||||
const {
|
const {
|
||||||
options: { prefixIdentifiers },
|
options: { prefixIdentifiers },
|
||||||
|
@ -41,22 +40,13 @@ export function genExpression(
|
||||||
return [[rawExpr, NewlineType.None, loc]]
|
return [[rawExpr, NewlineType.None, loc]]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ast === null) {
|
|
||||||
// the expression is a simple identifier
|
// the expression is a simple identifier
|
||||||
|
if (ast === null) {
|
||||||
return [genIdentifier(rawExpr, context, loc)]
|
return [genIdentifier(rawExpr, context, loc)]
|
||||||
}
|
}
|
||||||
|
|
||||||
const ids: Identifier[] = []
|
const ids: Identifier[] = []
|
||||||
walkIdentifiers(
|
walkIdentifiers(ast!, id => ids.push(id))
|
||||||
ast!,
|
|
||||||
(id, parent, parentStack, isReference, isLocal) => {
|
|
||||||
if (isLocal) return
|
|
||||||
ids.push(id)
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
[],
|
|
||||||
knownIds,
|
|
||||||
)
|
|
||||||
if (ids.length) {
|
if (ids.length) {
|
||||||
ids.sort((a, b) => a.start! - b.start!)
|
ids.sort((a, b) => a.start! - b.start!)
|
||||||
const [frag, push] = buildCodeFragment()
|
const [frag, push] = buildCodeFragment()
|
||||||
|
@ -92,11 +82,17 @@ const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
|
||||||
|
|
||||||
function genIdentifier(
|
function genIdentifier(
|
||||||
id: string,
|
id: string,
|
||||||
{ options, vaporHelper }: CodegenContext,
|
{ options, vaporHelper, identifiers }: CodegenContext,
|
||||||
loc?: SourceLocation,
|
loc?: SourceLocation,
|
||||||
): CodeFragment {
|
): CodeFragment {
|
||||||
const { inline, bindingMetadata } = options
|
const { inline, bindingMetadata } = options
|
||||||
let name: string | undefined = id
|
let name: string | undefined = id
|
||||||
|
|
||||||
|
const idMap = identifiers[id]
|
||||||
|
if (idMap && idMap.length) {
|
||||||
|
return [idMap[0], NewlineType.None, loc]
|
||||||
|
}
|
||||||
|
|
||||||
if (inline) {
|
if (inline) {
|
||||||
switch (bindingMetadata[id]) {
|
switch (bindingMetadata[id]) {
|
||||||
case BindingTypes.SETUP_REF:
|
case BindingTypes.SETUP_REF:
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { genBlockFunction } from './block'
|
||||||
|
import { genExpression } from './expression'
|
||||||
|
import {
|
||||||
|
type CodeFragment,
|
||||||
|
type CodegenContext,
|
||||||
|
buildCodeFragment,
|
||||||
|
} from '../generate'
|
||||||
|
import type { ForIRNode, IREffect } from '../ir'
|
||||||
|
import { genOperations } from './operation'
|
||||||
|
import { NewlineType } from '@vue/compiler-dom'
|
||||||
|
|
||||||
|
export function genFor(
|
||||||
|
oper: ForIRNode,
|
||||||
|
context: CodegenContext,
|
||||||
|
): CodeFragment[] {
|
||||||
|
const { newline, 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 idMap: Record<string, string> = {}
|
||||||
|
if (rawValue) idMap[rawValue] = `_block.s[0]`
|
||||||
|
if (rawKey) idMap[rawKey] = `_block.s[1]`
|
||||||
|
|
||||||
|
const blockRet = (): CodeFragment[] => [
|
||||||
|
`[n${render.dynamic.id!}, ${updateFn}]`,
|
||||||
|
]
|
||||||
|
|
||||||
|
const blockFn = context.withId(
|
||||||
|
() => genBlockFunction(render, context, ['_block'], blockRet),
|
||||||
|
idMap,
|
||||||
|
)
|
||||||
|
|
||||||
|
context.genEffect = undefined
|
||||||
|
|
||||||
|
return [
|
||||||
|
newline(),
|
||||||
|
`const n${oper.id} = `,
|
||||||
|
...call(vaporHelper('createFor'), sourceExpr, blockFn),
|
||||||
|
]
|
||||||
|
|
||||||
|
function genEffectInFor(effects: IREffect[]) {
|
||||||
|
if (!effects.length) {
|
||||||
|
updateFn = '() => {}'
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const [frag, push] = buildCodeFragment()
|
||||||
|
|
||||||
|
context.withIndent(() => {
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
newline(),
|
||||||
|
`const ${updateFn} = () => {`,
|
||||||
|
...frag,
|
||||||
|
newline(),
|
||||||
|
'}',
|
||||||
|
newline(),
|
||||||
|
`${vaporHelper('renderEffect')}(${updateFn})`,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { type IREffect, IRNodeTypes, type OperationNode } from '../ir'
|
||||||
|
import {
|
||||||
|
type CodeFragment,
|
||||||
|
type CodegenContext,
|
||||||
|
buildCodeFragment,
|
||||||
|
} from '../generate'
|
||||||
|
import { genAppendNode, genInsertNode, genPrependNode } from './dom'
|
||||||
|
import { genSetEvent } from './event'
|
||||||
|
import { genFor } from './for'
|
||||||
|
import { genSetHtml } from './html'
|
||||||
|
import { genIf } from './if'
|
||||||
|
import { genSetModelValue } from './modelValue'
|
||||||
|
import { genSetProp } from './prop'
|
||||||
|
import { genSetRef } from './ref'
|
||||||
|
import { genCreateTextNode, genSetText } from './text'
|
||||||
|
|
||||||
|
export function genOperations(opers: OperationNode[], ctx: CodegenContext) {
|
||||||
|
const [frag, push] = buildCodeFragment()
|
||||||
|
for (const operation of opers) {
|
||||||
|
push(...genOperation(operation, ctx))
|
||||||
|
}
|
||||||
|
return frag
|
||||||
|
}
|
||||||
|
|
||||||
|
function genOperation(
|
||||||
|
oper: OperationNode,
|
||||||
|
context: CodegenContext,
|
||||||
|
): CodeFragment[] {
|
||||||
|
switch (oper.type) {
|
||||||
|
case IRNodeTypes.SET_PROP:
|
||||||
|
return genSetProp(oper, context)
|
||||||
|
case IRNodeTypes.SET_TEXT:
|
||||||
|
return genSetText(oper, context)
|
||||||
|
case IRNodeTypes.SET_EVENT:
|
||||||
|
return genSetEvent(oper, context)
|
||||||
|
case IRNodeTypes.SET_HTML:
|
||||||
|
return genSetHtml(oper, context)
|
||||||
|
case IRNodeTypes.SET_REF:
|
||||||
|
return genSetRef(oper, context)
|
||||||
|
case IRNodeTypes.SET_MODEL_VALUE:
|
||||||
|
return genSetModelValue(oper, context)
|
||||||
|
case IRNodeTypes.CREATE_TEXT_NODE:
|
||||||
|
return genCreateTextNode(oper, context)
|
||||||
|
case IRNodeTypes.INSERT_NODE:
|
||||||
|
return genInsertNode(oper, context)
|
||||||
|
case IRNodeTypes.PREPEND_NODE:
|
||||||
|
return genPrependNode(oper, context)
|
||||||
|
case IRNodeTypes.APPEND_NODE:
|
||||||
|
return genAppendNode(oper, context)
|
||||||
|
case IRNodeTypes.IF:
|
||||||
|
return genIf(oper, context)
|
||||||
|
case IRNodeTypes.FOR:
|
||||||
|
return genFor(oper, context)
|
||||||
|
case IRNodeTypes.WITH_DIRECTIVE:
|
||||||
|
// TODO remove this after remove checkNever
|
||||||
|
// generated, skip
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return checkNever(oper)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
return frag
|
||||||
|
}
|
||||||
|
|
||||||
|
function genEffect({ operations }: IREffect, context: CodegenContext) {
|
||||||
|
const { newline, withIndent, vaporHelper } = context
|
||||||
|
const [frag, push] = buildCodeFragment()
|
||||||
|
push(newline(), `${vaporHelper('renderEffect')}(() => {`)
|
||||||
|
withIndent(() => {
|
||||||
|
operations.forEach(op => push(...genOperation(op, context)))
|
||||||
|
})
|
||||||
|
push(newline(), '})')
|
||||||
|
return frag
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove when stable
|
||||||
|
// @ts-expect-error
|
||||||
|
function checkNever(x: never): never {}
|
|
@ -13,3 +13,4 @@ export { transformOnce } from './transforms/vOnce'
|
||||||
export { transformVShow } from './transforms/vShow'
|
export { transformVShow } from './transforms/vShow'
|
||||||
export { transformVText } from './transforms/vText'
|
export { transformVText } from './transforms/vText'
|
||||||
export { transformVIf } from './transforms/vIf'
|
export { transformVIf } from './transforms/vIf'
|
||||||
|
export { transformVFor } from './transforms/vFor'
|
||||||
|
|
|
@ -12,6 +12,8 @@ import type { DirectiveTransform, NodeTransform } from './transform'
|
||||||
|
|
||||||
export enum IRNodeTypes {
|
export enum IRNodeTypes {
|
||||||
ROOT,
|
ROOT,
|
||||||
|
BLOCK_FUNCTION,
|
||||||
|
|
||||||
TEMPLATE_FACTORY,
|
TEMPLATE_FACTORY,
|
||||||
FRAGMENT_FACTORY,
|
FRAGMENT_FACTORY,
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ export enum IRNodeTypes {
|
||||||
WITH_DIRECTIVE,
|
WITH_DIRECTIVE,
|
||||||
|
|
||||||
IF,
|
IF,
|
||||||
BLOCK_FUNCTION,
|
FOR,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseIRNode {
|
export interface BaseIRNode {
|
||||||
|
@ -65,6 +67,16 @@ export interface IfIRNode extends BaseIRNode {
|
||||||
negative?: BlockFunctionIRNode | IfIRNode
|
negative?: BlockFunctionIRNode | IfIRNode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForIRNode extends BaseIRNode {
|
||||||
|
type: IRNodeTypes.FOR
|
||||||
|
id: number
|
||||||
|
source: IRExpression
|
||||||
|
value?: SimpleExpressionNode
|
||||||
|
key?: SimpleExpressionNode
|
||||||
|
index?: SimpleExpressionNode
|
||||||
|
render: BlockFunctionIRNode
|
||||||
|
}
|
||||||
|
|
||||||
export interface TemplateFactoryIRNode extends BaseIRNode {
|
export interface TemplateFactoryIRNode extends BaseIRNode {
|
||||||
type: IRNodeTypes.TEMPLATE_FACTORY
|
type: IRNodeTypes.TEMPLATE_FACTORY
|
||||||
template: string
|
template: string
|
||||||
|
@ -176,6 +188,7 @@ export type OperationNode =
|
||||||
| AppendNodeIRNode
|
| AppendNodeIRNode
|
||||||
| WithDirectiveIRNode
|
| WithDirectiveIRNode
|
||||||
| IfIRNode
|
| IfIRNode
|
||||||
|
| ForIRNode
|
||||||
|
|
||||||
export type BlockIRNode = RootIRNode | BlockFunctionIRNode
|
export type BlockIRNode = RootIRNode | BlockFunctionIRNode
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import {
|
import {
|
||||||
type AllNode,
|
type AllNode,
|
||||||
|
type AttributeNode,
|
||||||
type TransformOptions as BaseTransformOptions,
|
type TransformOptions as BaseTransformOptions,
|
||||||
type CompilerCompatOptions,
|
type CompilerCompatOptions,
|
||||||
|
type DirectiveNode,
|
||||||
type ElementNode,
|
type ElementNode,
|
||||||
ElementTypes,
|
ElementTypes,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
type ParentNode,
|
type ParentNode,
|
||||||
type RootNode,
|
type RootNode,
|
||||||
type TemplateChildNode,
|
type TemplateChildNode,
|
||||||
|
type TemplateNode,
|
||||||
defaultOnError,
|
defaultOnError,
|
||||||
defaultOnWarn,
|
defaultOnWarn,
|
||||||
isVSlot,
|
isVSlot,
|
||||||
|
@ -403,3 +406,27 @@ export function createStructuralDirectiveTransform(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
|
||||||
|
if (node.tagType === ElementTypes.TEMPLATE) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
const reserved: Array<AttributeNode | DirectiveNode> = []
|
||||||
|
const pass: Array<AttributeNode | DirectiveNode> = []
|
||||||
|
node.props.forEach(prop => {
|
||||||
|
if (prop.type === NodeTypes.DIRECTIVE && dirs.includes(prop.name)) {
|
||||||
|
reserved.push(prop)
|
||||||
|
} else {
|
||||||
|
pass.push(prop)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return extend({}, node, {
|
||||||
|
type: NodeTypes.ELEMENT,
|
||||||
|
tag: 'template',
|
||||||
|
props: reserved,
|
||||||
|
tagType: ElementTypes.TEMPLATE,
|
||||||
|
children: [extend({}, node, { props: pass } as TemplateChildNode)],
|
||||||
|
} as Partial<TemplateNode>)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {
|
||||||
|
type ElementNode,
|
||||||
|
ErrorCodes,
|
||||||
|
type SimpleExpressionNode,
|
||||||
|
createCompilerError,
|
||||||
|
} from '@vue/compiler-dom'
|
||||||
|
import {
|
||||||
|
type TransformContext,
|
||||||
|
createStructuralDirectiveTransform,
|
||||||
|
genDefaultDynamic,
|
||||||
|
wrapTemplate,
|
||||||
|
} from '../transform'
|
||||||
|
import {
|
||||||
|
type BlockFunctionIRNode,
|
||||||
|
DynamicFlag,
|
||||||
|
type IRDynamicInfo,
|
||||||
|
IRNodeTypes,
|
||||||
|
type VaporDirectiveNode,
|
||||||
|
} from '../ir'
|
||||||
|
import { extend } from '@vue/shared'
|
||||||
|
|
||||||
|
export const transformVFor = createStructuralDirectiveTransform(
|
||||||
|
'for',
|
||||||
|
processFor,
|
||||||
|
)
|
||||||
|
|
||||||
|
export function processFor(
|
||||||
|
node: ElementNode,
|
||||||
|
dir: VaporDirectiveNode,
|
||||||
|
context: TransformContext<ElementNode>,
|
||||||
|
) {
|
||||||
|
if (!dir.exp) {
|
||||||
|
context.options.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_V_FOR_NO_EXPRESSION, dir.loc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parseResult = dir.forParseResult
|
||||||
|
if (!parseResult) {
|
||||||
|
context.options.onError(
|
||||||
|
createCompilerError(ErrorCodes.X_V_FOR_MALFORMED_EXPRESSION, dir.loc),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { source, value, key, index } = parseResult
|
||||||
|
|
||||||
|
context.node = node = wrapTemplate(node, ['for'])
|
||||||
|
context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
|
||||||
|
const id = context.reference()
|
||||||
|
const render: BlockFunctionIRNode = {
|
||||||
|
type: IRNodeTypes.BLOCK_FUNCTION,
|
||||||
|
loc: node.loc,
|
||||||
|
node,
|
||||||
|
templateIndex: -1,
|
||||||
|
dynamic: extend(genDefaultDynamic(), {
|
||||||
|
flags: DynamicFlag.REFERENCED,
|
||||||
|
} satisfies Partial<IRDynamicInfo>),
|
||||||
|
effect: [],
|
||||||
|
operation: [],
|
||||||
|
}
|
||||||
|
const exitBlock = context.enterBlock(render)
|
||||||
|
context.reference()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
context.template += context.childrenTemplate.filter(Boolean).join('')
|
||||||
|
context.registerTemplate()
|
||||||
|
exitBlock()
|
||||||
|
context.registerOperation({
|
||||||
|
type: IRNodeTypes.FOR,
|
||||||
|
id,
|
||||||
|
loc: dir.loc,
|
||||||
|
source: source as SimpleExpressionNode,
|
||||||
|
value: value as SimpleExpressionNode | undefined,
|
||||||
|
key: key as SimpleExpressionNode | undefined,
|
||||||
|
index: index as SimpleExpressionNode | undefined,
|
||||||
|
render,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,8 @@
|
||||||
import {
|
import {
|
||||||
type ElementNode,
|
type ElementNode,
|
||||||
ElementTypes,
|
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
type TemplateChildNode,
|
type TemplateChildNode,
|
||||||
type TemplateNode,
|
|
||||||
createCompilerError,
|
createCompilerError,
|
||||||
createSimpleExpression,
|
createSimpleExpression,
|
||||||
} from '@vue/compiler-dom'
|
} from '@vue/compiler-dom'
|
||||||
|
@ -12,6 +10,7 @@ import {
|
||||||
type TransformContext,
|
type TransformContext,
|
||||||
createStructuralDirectiveTransform,
|
createStructuralDirectiveTransform,
|
||||||
genDefaultDynamic,
|
genDefaultDynamic,
|
||||||
|
wrapTemplate,
|
||||||
} from '../transform'
|
} from '../transform'
|
||||||
import {
|
import {
|
||||||
type BlockFunctionIRNode,
|
type BlockFunctionIRNode,
|
||||||
|
@ -117,7 +116,7 @@ export function processIf(
|
||||||
|
|
||||||
// TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
|
// TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
|
||||||
if (__DEV__ && comments.length) {
|
if (__DEV__ && comments.length) {
|
||||||
node = wrapTemplate(node)
|
node = wrapTemplate(node, ['else-if', 'else'])
|
||||||
context.node = node = extend({}, node, {
|
context.node = node = extend({}, node, {
|
||||||
children: [...comments, ...node.children],
|
children: [...comments, ...node.children],
|
||||||
})
|
})
|
||||||
|
@ -145,7 +144,7 @@ export function createIfBranch(
|
||||||
node: ElementNode,
|
node: ElementNode,
|
||||||
context: TransformContext<ElementNode>,
|
context: TransformContext<ElementNode>,
|
||||||
): [BlockFunctionIRNode, () => void] {
|
): [BlockFunctionIRNode, () => void] {
|
||||||
context.node = node = wrapTemplate(node)
|
context.node = node = wrapTemplate(node, ['if', 'else-if', 'else'])
|
||||||
|
|
||||||
const branch: BlockFunctionIRNode = {
|
const branch: BlockFunctionIRNode = {
|
||||||
type: IRNodeTypes.BLOCK_FUNCTION,
|
type: IRNodeTypes.BLOCK_FUNCTION,
|
||||||
|
@ -168,22 +167,3 @@ export function createIfBranch(
|
||||||
}
|
}
|
||||||
return [branch, onExit]
|
return [branch, onExit]
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapTemplate(node: ElementNode): TemplateNode {
|
|
||||||
if (node.tagType === ElementTypes.TEMPLATE) {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
return extend({}, node, {
|
|
||||||
type: NodeTypes.ELEMENT,
|
|
||||||
tag: 'template',
|
|
||||||
props: [],
|
|
||||||
tagType: ElementTypes.TEMPLATE,
|
|
||||||
children: [
|
|
||||||
extend({}, node, {
|
|
||||||
props: node.props.filter(
|
|
||||||
p => p.type !== NodeTypes.DIRECTIVE && p.name !== 'if',
|
|
||||||
),
|
|
||||||
} as TemplateChildNode),
|
|
||||||
],
|
|
||||||
} as Partial<TemplateNode>)
|
|
||||||
}
|
|
||||||
|
|
|
@ -229,16 +229,18 @@ export function baseWatch(
|
||||||
getter = () => traverse(baseGetter())
|
getter = () => traverse(baseGetter())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scope = getCurrentScope()
|
||||||
|
|
||||||
if (once) {
|
if (once) {
|
||||||
if (!cb) {
|
if (!cb) {
|
||||||
// onEffectCleanup need use effect as a key
|
// onEffectCleanup need use effect as a key
|
||||||
getCurrentScope()?.effects.push((effect = {} as any))
|
scope?.effects.push((effect = {} as any))
|
||||||
getter()
|
getter()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
// onEffectCleanup need use effect as a key
|
// onEffectCleanup need use effect as a key
|
||||||
getCurrentScope()?.effects.push((effect = {} as any))
|
scope?.effects.push((effect = {} as any))
|
||||||
callWithAsyncErrorHandling(
|
callWithAsyncErrorHandling(
|
||||||
cb,
|
cb,
|
||||||
onError,
|
onError,
|
||||||
|
@ -317,7 +319,7 @@ export function baseWatch(
|
||||||
|
|
||||||
let effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
|
let effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
|
||||||
|
|
||||||
effect = new ReactiveEffect(getter, NOOP, effectScheduler)
|
effect = new ReactiveEffect(getter, NOOP, effectScheduler, scope)
|
||||||
|
|
||||||
cleanup = effect.onStop = () => {
|
cleanup = effect.onStop = () => {
|
||||||
const cleanups = cleanupMap.get(effect)
|
const cleanups = cleanupMap.get(effect)
|
||||||
|
|
|
@ -70,7 +70,7 @@ export class ReactiveEffect<T = any> {
|
||||||
public fn: () => T,
|
public fn: () => T,
|
||||||
public trigger: () => void,
|
public trigger: () => void,
|
||||||
public scheduler?: EffectScheduler,
|
public scheduler?: EffectScheduler,
|
||||||
scope?: EffectScope,
|
public scope?: EffectScope,
|
||||||
) {
|
) {
|
||||||
recordEffectScope(this, scope)
|
recordEffectScope(this, scope)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
import { getCurrentEffect, onEffectCleanup } from '@vue/reactivity'
|
import {
|
||||||
|
getCurrentEffect,
|
||||||
|
getCurrentScope,
|
||||||
|
onEffectCleanup,
|
||||||
|
onScopeDispose,
|
||||||
|
} from '@vue/reactivity'
|
||||||
import { recordPropMetadata } from './patchProp'
|
import { recordPropMetadata } from './patchProp'
|
||||||
import { toHandlerKey } from '@vue/shared'
|
import { toHandlerKey } from '@vue/shared'
|
||||||
|
|
||||||
|
@ -10,7 +15,14 @@ export function on(
|
||||||
) {
|
) {
|
||||||
recordPropMetadata(el, toHandlerKey(event), handler)
|
recordPropMetadata(el, toHandlerKey(event), handler)
|
||||||
el.addEventListener(event, handler, options)
|
el.addEventListener(event, handler, options)
|
||||||
if (getCurrentEffect()) {
|
|
||||||
onEffectCleanup(() => el.removeEventListener(event, handler, options))
|
const scope = getCurrentScope()
|
||||||
|
const effect = getCurrentEffect()
|
||||||
|
|
||||||
|
const cleanup = () => el.removeEventListener(event, handler, options)
|
||||||
|
if (effect && effect.scope === scope) {
|
||||||
|
onEffectCleanup(cleanup)
|
||||||
|
} else if (scope) {
|
||||||
|
onScopeDispose(cleanup)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const name = 'click'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button @[name]="">click me</button>
|
||||||
|
</template>
|
|
@ -1,6 +1,12 @@
|
||||||
import { render } from 'vue/vapor'
|
import { render, unmountComponent } from 'vue/vapor'
|
||||||
|
|
||||||
const modules = import.meta.glob<any>('./*.(vue|js)')
|
const modules = import.meta.glob<any>('./*.(vue|js)')
|
||||||
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
|
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
|
||||||
|
|
||||||
mod.then(({ default: mod }) => render(mod, {}, '#app'))
|
mod.then(({ default: mod }) => {
|
||||||
|
const instance = render(mod, {}, '#app')
|
||||||
|
// @ts-expect-error
|
||||||
|
globalThis.unmount = () => {
|
||||||
|
unmountComponent(instance)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
@ -34,7 +34,8 @@ function handleClearAll() {
|
||||||
tasks.value = []
|
tasks.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRemove(idx: number) {
|
function handleRemove(idx: number, task: Task) {
|
||||||
|
console.log(task)
|
||||||
tasks.value.splice(idx, 1)
|
tasks.value.splice(idx, 1)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -42,42 +43,18 @@ function handleRemove(idx: number) {
|
||||||
<template>
|
<template>
|
||||||
<h1>todos</h1>
|
<h1>todos</h1>
|
||||||
<ul>
|
<ul>
|
||||||
<!-- TODO: v-for -->
|
<li
|
||||||
<li v-if="tasks[0]" :class="{ del: tasks[0]?.completed }">
|
v-for="(task, index) of tasks"
|
||||||
|
:key="index"
|
||||||
|
:class="{ del: task.completed }"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="tasks[0]?.completed"
|
:checked="task.completed"
|
||||||
@change="handleComplete(0, $event)"
|
@change="handleComplete(index, $event)"
|
||||||
/>
|
/>
|
||||||
{{ tasks[0]?.title }}
|
{{ task.title }}
|
||||||
<button @click="handleRemove(0)">x</button>
|
<button @click="handleRemove(index, task)">x</button>
|
||||||
</li>
|
|
||||||
<li v-if="tasks[1]" :class="{ del: tasks[1]?.completed }">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="tasks[1]?.completed"
|
|
||||||
@change="handleComplete(1, $event)"
|
|
||||||
/>
|
|
||||||
{{ tasks[1]?.title }}
|
|
||||||
<button @click="handleRemove(1)">x</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="tasks[2]" :class="{ del: tasks[2]?.completed }">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="tasks[2]?.completed"
|
|
||||||
@change="handleComplete(2, $event)"
|
|
||||||
/>
|
|
||||||
{{ tasks[2]?.title }}
|
|
||||||
<button @click="handleRemove(2)">x</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="tasks[3]" :class="{ del: tasks[3]?.completed }">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="tasks[3]?.completed"
|
|
||||||
@change="handleComplete(3, $event)"
|
|
||||||
/>
|
|
||||||
{{ tasks[3]?.title }}
|
|
||||||
<button @click="handleRemove(3)">x</button>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
|
|
Loading…
Reference in New Issue