feat(compiler-vapor): v-for (#101)

This commit is contained in:
三咲智子 Kevin Deng 2024-01-31 17:00:19 +08:00 committed by GitHub
parent 7b036fd4c0
commit 681dc5d954
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 513 additions and 157 deletions

View File

@ -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
}"
`;

View File

@ -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()
})
})

View File

@ -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,
], ],
{ {

View File

@ -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 {}

View File

@ -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
} }

View File

@ -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 {

View File

@ -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:

View File

@ -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})`,
]
}
}

View File

@ -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 {}

View File

@ -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'

View File

@ -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

View File

@ -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>)
}

View File

@ -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,
})
}
}

View File

@ -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>)
}

View File

@ -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)

View File

@ -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)
} }

View File

@ -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)
} }
} }

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
const name = 'click'
</script>
<template>
<button @[name]="">click me</button>
</template>

View File

@ -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)
}
})

View File

@ -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>