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 { transformVModel } from './transforms/vModel'
|
||||
import { transformVIf } from './transforms/vIf'
|
||||
import { transformVFor } from './transforms/vFor'
|
||||
|
||||
export type CompilerOptions = HackOptions<BaseCompilerOptions>
|
||||
|
||||
|
@ -102,6 +103,7 @@ export function getBaseTransformPreset(
|
|||
transformRef,
|
||||
transformInterpolation,
|
||||
transformVIf,
|
||||
transformVFor,
|
||||
transformElement,
|
||||
],
|
||||
{
|
||||
|
|
|
@ -7,23 +7,10 @@ import {
|
|||
advancePositionWithMutation,
|
||||
locStub,
|
||||
} from '@vue/compiler-dom'
|
||||
import {
|
||||
IRNodeTypes,
|
||||
type OperationNode,
|
||||
type RootIRNode,
|
||||
type VaporHelper,
|
||||
} from './ir'
|
||||
import type { IREffect, RootIRNode, VaporHelper } from './ir'
|
||||
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 { 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 { genBlockFunctionContent } from './generators/block'
|
||||
|
||||
|
@ -89,6 +76,23 @@ export class CodegenContext {
|
|||
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) {
|
||||
const defaultOptions = {
|
||||
mode: 'function',
|
||||
|
@ -270,45 +274,3 @@ export function buildCodeFragment() {
|
|||
const push = frag.push.bind(frag)
|
||||
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 CodegenContext,
|
||||
buildCodeFragment,
|
||||
genOperation,
|
||||
} from '../generate'
|
||||
import { genWithDirective } from './directive'
|
||||
import { genEffects, genOperations } from './operation'
|
||||
|
||||
export function genBlockFunction(
|
||||
oper: BlockFunctionIRNode,
|
||||
context: CodegenContext,
|
||||
args: CodeFragment[] = [],
|
||||
returnValue?: () => CodeFragment[],
|
||||
): CodeFragment[] {
|
||||
const { newline, withIndent } = context
|
||||
return [
|
||||
'() => {',
|
||||
...withIndent(() => genBlockFunctionContent(oper, context)),
|
||||
'(',
|
||||
...args,
|
||||
') => {',
|
||||
...withIndent(() => genBlockFunctionContent(oper, context, returnValue)),
|
||||
newline(),
|
||||
'}',
|
||||
]
|
||||
|
@ -30,8 +34,9 @@ export function genBlockFunction(
|
|||
export function genBlockFunctionContent(
|
||||
ir: BlockFunctionIRNode | RootIRNode,
|
||||
ctx: CodegenContext,
|
||||
returnValue?: () => CodeFragment[],
|
||||
): CodeFragment[] {
|
||||
const { newline, withIndent, vaporHelper } = ctx
|
||||
const { newline, vaporHelper } = ctx
|
||||
const [frag, push] = buildCodeFragment()
|
||||
|
||||
push(newline(), `const n${ir.dynamic.id} = t${ir.templateIndex}()`)
|
||||
|
@ -52,19 +57,14 @@ export function genBlockFunctionContent(
|
|||
push(...genWithDirective(directives, ctx))
|
||||
}
|
||||
|
||||
for (const operation of ir.operation) {
|
||||
push(...genOperation(operation, ctx))
|
||||
}
|
||||
push(...genOperations(ir.operation, ctx))
|
||||
push(...genEffects(ir.effect, ctx))
|
||||
|
||||
for (const { operations } of ir.effect) {
|
||||
push(newline(), `${vaporHelper('renderEffect')}(() => {`)
|
||||
withIndent(() => {
|
||||
operations.forEach(op => push(...genOperation(op, ctx)))
|
||||
})
|
||||
push(newline(), '})')
|
||||
}
|
||||
|
||||
push(newline(), `return n${ir.dynamic.id}`)
|
||||
push(
|
||||
newline(),
|
||||
'return ',
|
||||
...(returnValue ? returnValue() : [`n${ir.dynamic.id}`]),
|
||||
)
|
||||
|
||||
return frag
|
||||
}
|
||||
|
|
|
@ -45,13 +45,13 @@ export function genSetEvent(
|
|||
const hasMultipleStatements = exp.content.includes(`;`)
|
||||
|
||||
if (isInlineStatement) {
|
||||
const knownIds = Object.create(null)
|
||||
knownIds['$event'] = 1
|
||||
|
||||
const expr = context.withId(() => genExpression(exp, context), {
|
||||
$event: null,
|
||||
})
|
||||
return [
|
||||
'$event => ',
|
||||
hasMultipleStatements ? '{' : '(',
|
||||
...genExpression(exp, context, knownIds),
|
||||
...expr,
|
||||
hasMultipleStatements ? '}' : ')',
|
||||
]
|
||||
} else {
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
export function genExpression(
|
||||
node: IRExpression,
|
||||
context: CodegenContext,
|
||||
knownIds: Record<string, number> = Object.create(null),
|
||||
): CodeFragment[] {
|
||||
const {
|
||||
options: { prefixIdentifiers },
|
||||
|
@ -41,22 +40,13 @@ export function genExpression(
|
|||
return [[rawExpr, NewlineType.None, loc]]
|
||||
}
|
||||
|
||||
// the expression is a simple identifier
|
||||
if (ast === null) {
|
||||
// the expression is a simple identifier
|
||||
return [genIdentifier(rawExpr, context, loc)]
|
||||
}
|
||||
|
||||
const ids: Identifier[] = []
|
||||
walkIdentifiers(
|
||||
ast!,
|
||||
(id, parent, parentStack, isReference, isLocal) => {
|
||||
if (isLocal) return
|
||||
ids.push(id)
|
||||
},
|
||||
false,
|
||||
[],
|
||||
knownIds,
|
||||
)
|
||||
walkIdentifiers(ast!, id => ids.push(id))
|
||||
if (ids.length) {
|
||||
ids.sort((a, b) => a.start! - b.start!)
|
||||
const [frag, push] = buildCodeFragment()
|
||||
|
@ -92,11 +82,17 @@ const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
|
|||
|
||||
function genIdentifier(
|
||||
id: string,
|
||||
{ options, vaporHelper }: CodegenContext,
|
||||
{ options, vaporHelper, identifiers }: CodegenContext,
|
||||
loc?: SourceLocation,
|
||||
): CodeFragment {
|
||||
const { inline, bindingMetadata } = options
|
||||
let name: string | undefined = id
|
||||
|
||||
const idMap = identifiers[id]
|
||||
if (idMap && idMap.length) {
|
||||
return [idMap[0], NewlineType.None, loc]
|
||||
}
|
||||
|
||||
if (inline) {
|
||||
switch (bindingMetadata[id]) {
|
||||
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 { transformVText } from './transforms/vText'
|
||||
export { transformVIf } from './transforms/vIf'
|
||||
export { transformVFor } from './transforms/vFor'
|
||||
|
|
|
@ -12,6 +12,8 @@ import type { DirectiveTransform, NodeTransform } from './transform'
|
|||
|
||||
export enum IRNodeTypes {
|
||||
ROOT,
|
||||
BLOCK_FUNCTION,
|
||||
|
||||
TEMPLATE_FACTORY,
|
||||
FRAGMENT_FACTORY,
|
||||
|
||||
|
@ -30,7 +32,7 @@ export enum IRNodeTypes {
|
|||
WITH_DIRECTIVE,
|
||||
|
||||
IF,
|
||||
BLOCK_FUNCTION,
|
||||
FOR,
|
||||
}
|
||||
|
||||
export interface BaseIRNode {
|
||||
|
@ -65,6 +67,16 @@ export interface IfIRNode extends BaseIRNode {
|
|||
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 {
|
||||
type: IRNodeTypes.TEMPLATE_FACTORY
|
||||
template: string
|
||||
|
@ -176,6 +188,7 @@ export type OperationNode =
|
|||
| AppendNodeIRNode
|
||||
| WithDirectiveIRNode
|
||||
| IfIRNode
|
||||
| ForIRNode
|
||||
|
||||
export type BlockIRNode = RootIRNode | BlockFunctionIRNode
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import {
|
||||
type AllNode,
|
||||
type AttributeNode,
|
||||
type TransformOptions as BaseTransformOptions,
|
||||
type CompilerCompatOptions,
|
||||
type DirectiveNode,
|
||||
type ElementNode,
|
||||
ElementTypes,
|
||||
NodeTypes,
|
||||
type ParentNode,
|
||||
type RootNode,
|
||||
type TemplateChildNode,
|
||||
type TemplateNode,
|
||||
defaultOnError,
|
||||
defaultOnWarn,
|
||||
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 {
|
||||
type ElementNode,
|
||||
ElementTypes,
|
||||
ErrorCodes,
|
||||
NodeTypes,
|
||||
type TemplateChildNode,
|
||||
type TemplateNode,
|
||||
createCompilerError,
|
||||
createSimpleExpression,
|
||||
} from '@vue/compiler-dom'
|
||||
|
@ -12,6 +10,7 @@ import {
|
|||
type TransformContext,
|
||||
createStructuralDirectiveTransform,
|
||||
genDefaultDynamic,
|
||||
wrapTemplate,
|
||||
} from '../transform'
|
||||
import {
|
||||
type BlockFunctionIRNode,
|
||||
|
@ -117,7 +116,7 @@ export function processIf(
|
|||
|
||||
// TODO ignore comments if the v-if is direct child of <transition> (PR #3622)
|
||||
if (__DEV__ && comments.length) {
|
||||
node = wrapTemplate(node)
|
||||
node = wrapTemplate(node, ['else-if', 'else'])
|
||||
context.node = node = extend({}, node, {
|
||||
children: [...comments, ...node.children],
|
||||
})
|
||||
|
@ -145,7 +144,7 @@ export function createIfBranch(
|
|||
node: ElementNode,
|
||||
context: TransformContext<ElementNode>,
|
||||
): [BlockFunctionIRNode, () => void] {
|
||||
context.node = node = wrapTemplate(node)
|
||||
context.node = node = wrapTemplate(node, ['if', 'else-if', 'else'])
|
||||
|
||||
const branch: BlockFunctionIRNode = {
|
||||
type: IRNodeTypes.BLOCK_FUNCTION,
|
||||
|
@ -168,22 +167,3 @@ export function createIfBranch(
|
|||
}
|
||||
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())
|
||||
}
|
||||
|
||||
const scope = getCurrentScope()
|
||||
|
||||
if (once) {
|
||||
if (!cb) {
|
||||
// onEffectCleanup need use effect as a key
|
||||
getCurrentScope()?.effects.push((effect = {} as any))
|
||||
scope?.effects.push((effect = {} as any))
|
||||
getter()
|
||||
return
|
||||
}
|
||||
if (immediate) {
|
||||
// onEffectCleanup need use effect as a key
|
||||
getCurrentScope()?.effects.push((effect = {} as any))
|
||||
scope?.effects.push((effect = {} as any))
|
||||
callWithAsyncErrorHandling(
|
||||
cb,
|
||||
onError,
|
||||
|
@ -317,7 +319,7 @@ export function baseWatch(
|
|||
|
||||
let effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
|
||||
|
||||
effect = new ReactiveEffect(getter, NOOP, effectScheduler)
|
||||
effect = new ReactiveEffect(getter, NOOP, effectScheduler, scope)
|
||||
|
||||
cleanup = effect.onStop = () => {
|
||||
const cleanups = cleanupMap.get(effect)
|
||||
|
|
|
@ -70,7 +70,7 @@ export class ReactiveEffect<T = any> {
|
|||
public fn: () => T,
|
||||
public trigger: () => void,
|
||||
public scheduler?: EffectScheduler,
|
||||
scope?: EffectScope,
|
||||
public scope?: EffectScope,
|
||||
) {
|
||||
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 { toHandlerKey } from '@vue/shared'
|
||||
|
||||
|
@ -10,7 +15,14 @@ export function on(
|
|||
) {
|
||||
recordPropMetadata(el, toHandlerKey(event), handler)
|
||||
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 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 = []
|
||||
}
|
||||
|
||||
function handleRemove(idx: number) {
|
||||
function handleRemove(idx: number, task: Task) {
|
||||
console.log(task)
|
||||
tasks.value.splice(idx, 1)
|
||||
}
|
||||
</script>
|
||||
|
@ -42,42 +43,18 @@ function handleRemove(idx: number) {
|
|||
<template>
|
||||
<h1>todos</h1>
|
||||
<ul>
|
||||
<!-- TODO: v-for -->
|
||||
<li v-if="tasks[0]" :class="{ del: tasks[0]?.completed }">
|
||||
<li
|
||||
v-for="(task, index) of tasks"
|
||||
:key="index"
|
||||
:class="{ del: task.completed }"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="tasks[0]?.completed"
|
||||
@change="handleComplete(0, $event)"
|
||||
:checked="task.completed"
|
||||
@change="handleComplete(index, $event)"
|
||||
/>
|
||||
{{ tasks[0]?.title }}
|
||||
<button @click="handleRemove(0)">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>
|
||||
{{ task.title }}
|
||||
<button @click="handleRemove(index, task)">x</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
|
Loading…
Reference in New Issue