From: 三咲智子 Kevin Deng Date: Wed, 31 Jan 2024 09:00:19 +0000 (+0800) Subject: feat(compiler-vapor): v-for (#101) X-Git-Tag: v3.6.0-alpha.1~16^2~623 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=681dc5d954b5421d94943530a5ecefe1bda58b88;p=thirdparty%2Fvuejs%2Fcore.git feat(compiler-vapor): v-for (#101) --- diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap new file mode 100644 index 0000000000..ed3f049073 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -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("
") + 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("
item
") + const t1 = _fragment() + const n0 = t1() + const n1 = _createFor(() => (_ctx.items), (_block) => { + const n2 = t0() + return [n2, () => {}] + }) + _append(n0, n1) + return n0 +}" +`; diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts new file mode 100644 index 0000000000..bfa31ab537 --- /dev/null +++ b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts @@ -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( + `
{{ item }}
`, + ) + + expect(code).matchSnapshot() + expect(vaporHelpers).contains('createFor') + expect(helpers.size).toBe(0) + expect(ir.template).lengthOf(2) + expect(ir.template).toMatchObject([ + { + template: '
', + 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(`
item
`) + expect(code).matchSnapshot() + }) +}) diff --git a/packages/compiler-vapor/src/compile.ts b/packages/compiler-vapor/src/compile.ts index 0c7b139456..dc8109cf96 100644 --- a/packages/compiler-vapor/src/compile.ts +++ b/packages/compiler-vapor/src/compile.ts @@ -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 @@ -102,6 +103,7 @@ export function getBaseTransformPreset( transformRef, transformInterpolation, transformVIf, + transformVFor, transformElement, ], { diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 04ba18b042..5cb27aff36 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -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 = Object.create(null) + withId = (fn: () => T, map: Record): 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 {} diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts index bc0ecc840f..5316b1ad28 100644 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@ -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)) - } - - for (const { operations } of ir.effect) { - push(newline(), `${vaporHelper('renderEffect')}(() => {`) - withIndent(() => { - operations.forEach(op => push(...genOperation(op, ctx))) - }) - push(newline(), '})') - } + push(...genOperations(ir.operation, ctx)) + push(...genEffects(ir.effect, ctx)) - push(newline(), `return n${ir.dynamic.id}`) + push( + newline(), + 'return ', + ...(returnValue ? returnValue() : [`n${ir.dynamic.id}`]), + ) return frag } diff --git a/packages/compiler-vapor/src/generators/event.ts b/packages/compiler-vapor/src/generators/event.ts index e8e6d30dd6..e6695f0a4e 100644 --- a/packages/compiler-vapor/src/generators/event.ts +++ b/packages/compiler-vapor/src/generators/event.ts @@ -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 { diff --git a/packages/compiler-vapor/src/generators/expression.ts b/packages/compiler-vapor/src/generators/expression.ts index 742811ea86..8cf5f30e77 100644 --- a/packages/compiler-vapor/src/generators/expression.ts +++ b/packages/compiler-vapor/src/generators/expression.ts @@ -17,7 +17,6 @@ import { export function genExpression( node: IRExpression, context: CodegenContext, - knownIds: Record = 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: diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts new file mode 100644 index 0000000000..6514686abe --- /dev/null +++ b/packages/compiler-vapor/src/generators/for.ts @@ -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 = {} + 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 = {} + 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})`, + ] + } +} diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts new file mode 100644 index 0000000000..3e414095f3 --- /dev/null +++ b/packages/compiler-vapor/src/generators/operation.ts @@ -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 {} diff --git a/packages/compiler-vapor/src/index.ts b/packages/compiler-vapor/src/index.ts index 2402d410ad..14b7eca065 100644 --- a/packages/compiler-vapor/src/index.ts +++ b/packages/compiler-vapor/src/index.ts @@ -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' diff --git a/packages/compiler-vapor/src/ir.ts b/packages/compiler-vapor/src/ir.ts index 1c8f101564..1c9596f35a 100644 --- a/packages/compiler-vapor/src/ir.ts +++ b/packages/compiler-vapor/src/ir.ts @@ -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 diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index b6e231f92b..00078f57ce 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -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 = [] + const pass: Array = [] + 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) +} diff --git a/packages/compiler-vapor/src/transforms/vFor.ts b/packages/compiler-vapor/src/transforms/vFor.ts new file mode 100644 index 0000000000..dd61405e3d --- /dev/null +++ b/packages/compiler-vapor/src/transforms/vFor.ts @@ -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, +) { + 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), + 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, + }) + } +} diff --git a/packages/compiler-vapor/src/transforms/vIf.ts b/packages/compiler-vapor/src/transforms/vIf.ts index 03bb22b159..9c9731a81e 100644 --- a/packages/compiler-vapor/src/transforms/vIf.ts +++ b/packages/compiler-vapor/src/transforms/vIf.ts @@ -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 (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, ): [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) -} diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index f6e20cfa52..3f8c19784e 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -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) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index a41cd4986f..7f916e0a64 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -70,7 +70,7 @@ export class ReactiveEffect { public fn: () => T, public trigger: () => void, public scheduler?: EffectScheduler, - scope?: EffectScope, + public scope?: EffectScope, ) { recordEffectScope(this, scope) } diff --git a/packages/runtime-vapor/src/dom/on.ts b/packages/runtime-vapor/src/dom/on.ts index 47d7bfe1a5..f0d729165a 100644 --- a/packages/runtime-vapor/src/dom/on.ts +++ b/packages/runtime-vapor/src/dom/on.ts @@ -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) } } diff --git a/playground/src/dynamic-on.vue b/playground/src/dynamic-on.vue new file mode 100644 index 0000000000..871885eb92 --- /dev/null +++ b/playground/src/dynamic-on.vue @@ -0,0 +1,7 @@ + + + diff --git a/playground/src/main.ts b/playground/src/main.ts index 717629057a..8e52221258 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -1,6 +1,12 @@ -import { render } from 'vue/vapor' +import { render, unmountComponent } from 'vue/vapor' const modules = import.meta.glob('./*.(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) + } +}) diff --git a/playground/src/todo-mvc.vue b/playground/src/todo-mvc.vue index ed505080ad..63192d01ba 100644 --- a/playground/src/todo-mvc.vue +++ b/playground/src/todo-mvc.vue @@ -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) } @@ -42,42 +43,18 @@ function handleRemove(idx: number) {