]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-vapor): v-for (#101)
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Wed, 31 Jan 2024 09:00:19 +0000 (17:00 +0800)
committerGitHub <noreply@github.com>
Wed, 31 Jan 2024 09:00:19 +0000 (17:00 +0800)
20 files changed:
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap [new file with mode: 0644]
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts [new file with mode: 0644]
packages/compiler-vapor/src/compile.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/block.ts
packages/compiler-vapor/src/generators/event.ts
packages/compiler-vapor/src/generators/expression.ts
packages/compiler-vapor/src/generators/for.ts [new file with mode: 0644]
packages/compiler-vapor/src/generators/operation.ts [new file with mode: 0644]
packages/compiler-vapor/src/index.ts
packages/compiler-vapor/src/ir.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/vFor.ts [new file with mode: 0644]
packages/compiler-vapor/src/transforms/vIf.ts
packages/reactivity/src/baseWatch.ts
packages/reactivity/src/effect.ts
packages/runtime-vapor/src/dom/on.ts
playground/src/dynamic-on.vue [new file with mode: 0644]
playground/src/main.ts
playground/src/todo-mvc.vue

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 (file)
index 0000000..ed3f049
--- /dev/null
@@ -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
+}"
+`;
diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
new file mode 100644 (file)
index 0000000..bfa31ab
--- /dev/null
@@ -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()
+  })
+})
index 0c7b139456fd9689cc275965a12c2128d097d56f..dc8109cf964433717841624208a4d5852233826b 100644 (file)
@@ -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,
     ],
     {
index 04ba18b04253d517fd7358606879bbc77d92b63a..5cb27aff367f579d9f2fbeb013cd4996ad227476 100644 (file)
@@ -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 {}
index bc0ecc840fce513b3c869c38ea76f9c7c4d7181c..5316b1ad288c273e3e70f126f01a3a1d6bbdb6ac 100644 (file)
@@ -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
 }
index e8e6d30dd6606bfaf0df0f7cbda5e059623ce757..e6695f0a4ece0502bf91d1717f22f90ec6fee9ee 100644 (file)
@@ -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 {
index 742811ea866728dedd70570a604beec856fdc66f..8cf5f30e775e8acf07165e5657d3c61bb6571ed2 100644 (file)
@@ -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:
diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts
new file mode 100644 (file)
index 0000000..6514686
--- /dev/null
@@ -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})`,
+    ]
+  }
+}
diff --git a/packages/compiler-vapor/src/generators/operation.ts b/packages/compiler-vapor/src/generators/operation.ts
new file mode 100644 (file)
index 0000000..3e41409
--- /dev/null
@@ -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 {}
index 2402d410ad99576067b10cda7369cb73ddec6c57..14b7eca065c89c43141eda9bc2f300ea3771642c 100644 (file)
@@ -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'
index 1c8f101564ea213df9068eec7400d89ca310a158..1c9596f35a0aaf33a2f4cb0bc0de5c35c2c21b64 100644 (file)
@@ -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
 
index b6e231f92b909c5432ee21693aa1c46d3c76ecb3..00078f57ce3dce257b11bc45ed68a0c5070c2cb8 100644 (file)
@@ -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>)
+}
diff --git a/packages/compiler-vapor/src/transforms/vFor.ts b/packages/compiler-vapor/src/transforms/vFor.ts
new file mode 100644 (file)
index 0000000..dd61405
--- /dev/null
@@ -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,
+    })
+  }
+}
index 03bb22b159baf6855ffec035451dcc376efa3b39..9c9731a81e41dc4ee36070930158ddeb200a0fce 100644 (file)
@@ -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>)
-}
index f6e20cfa52cea09a264e7994d4afb40c9ceaa99c..3f8c19784ec0df06fac62fafc29daa596b89006f 100644 (file)
@@ -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)
index a41cd4986f61cd65718ca1c195eb4fe9de28216e..7f916e0a640ff58057b323507d6d3a6e391bd667 100644 (file)
@@ -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)
   }
index 47d7bfe1a5f785d7e70c078492cf85d4677d0697..f0d729165a6113bd878608f7384277d562ebacdd 100644 (file)
@@ -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 (file)
index 0000000..871885e
--- /dev/null
@@ -0,0 +1,7 @@
+<script setup lang="ts">
+const name = 'click'
+</script>
+
+<template>
+  <button @[name]="">click me</button>
+</template>
index 717629057a3e7988637c28b37aa4f8fcbe5c30dd..8e522212583e5546494d7d055298ac36b0b556f7 100644 (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 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)
+  }
+})
index ed505080ad9c3452af38aef6400c9a3466a1108e..63192d01ba789753dc78eaff8f72c32bb330c10d 100644 (file)
@@ -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>