]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: v-for destructure expression rewrite (part 1)
authorEvan You <evan@vuejs.org>
Thu, 30 Jan 2025 12:06:41 +0000 (20:06 +0800)
committerEvan You <evan@vuejs.org>
Thu, 30 Jan 2025 12:06:41 +0000 (20:06 +0800)
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
packages/compiler-vapor/src/generators/for.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/vFor.ts

index 4edbec43f005f4810b60d2672c70900b688df085..d67038943bee732a6cfe60703d8a555498d41d10 100644 (file)
@@ -1,15 +1,15 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`compiler: v-for > array de-structured value 1`] = `
-"import { setText as _setText, renderEffect as _renderEffect, withDestructure as _withDestructure, createFor as _createFor, template as _template } from 'vue';
+"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div></div>", true)
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.list), _withDestructure(([[id, ...other], index]) => [id, other, index], (_ctx0) => {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
     const n2 = t0()
-    _renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]))
+    _renderEffect(() => _setText(n2, _ctx0[0].value[0] + _ctx0[0].value[1] + _ctx0[1].value))
     return n2
-  }), ([id, ...other], index) => (id))
+  }, ([id, other], index) => (id))
   return n0
 }"
 `;
@@ -73,7 +73,7 @@ export function render(_ctx) {
       const n4 = t0()
       _renderEffect(() => _setText(n4, _ctx1[0].value+_ctx0[0].value))
       return n4
-    }, null, n5)
+    })
     _insert(n2, n5)
     return n5
   })
@@ -81,16 +81,44 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: v-for > object de-structured value (with rest) 1`] = `
+"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<div></div>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
+    const n2 = t0()
+    _renderEffect(() => _setText(n2, _ctx0[0].value.id + _ctx0[0].value + _ctx0[1].value))
+    return n2
+  }, ({ id, ...other }, index) => (id))
+  return n0
+}"
+`;
+
 exports[`compiler: v-for > object de-structured value 1`] = `
-"import { setText as _setText, renderEffect as _renderEffect, withDestructure as _withDestructure, createFor as _createFor, template as _template } from 'vue';
+"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
+const t0 = _template("<span></span>", true)
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
+    const n2 = t0()
+    _renderEffect(() => _setText(n2, _ctx0[0].value.id, _ctx0[0].value.value))
+    return n2
+  }, ({ id, value }) => (id))
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > object de-structured value 2`] = `
+"import { setText as _setText, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue';
 const t0 = _template("<div></div>", true)
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.list), _withDestructure(([{ id, ...other }, index]) => [id, other, index], (_ctx0) => {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
     const n2 = t0()
-    _renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]))
+    _renderEffect(() => _setText(n2, _ctx0[0].value.id + _ctx0[0].value + _ctx0[1].value))
     return n2
-  }), ({ id, ...other }, index) => (id))
+  }, ({ id, ...other }, index) => (id))
   return n0
 }"
 `;
index 39384169e1038b2b16e93ba7f927da4ea3e6f578..15fcd2182a8a4343cb8e90e8f2dbfd650e5b23d5 100644 (file)
@@ -68,7 +68,7 @@ export function render(_ctx) {
   const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
     const n2 = t0()
     return n2
-  }, null, null, null, true)
+  }, null, null, true)
   return n0
 }"
 `;
index f0de4c32f5079ff1e3beb948a3157f75366ce7b5..e14380d766668c205b0a88b68964df3f29051d7c 100644 (file)
@@ -79,13 +79,6 @@ describe('compiler: v-for', () => {
     expect(code).matchSnapshot()
   })
 
-  test.todo('object de-structured value', () => {
-    const { code } = compileWithVFor(
-      '<span v-for="({ id, value }) in items">{{ id }}{{ value }}</span>',
-    )
-    expect(code).matchSnapshot()
-  })
-
   test('nested v-for', () => {
     const { code, ir } = compileWithVFor(
       `<div v-for="i in list"><span v-for="j in i">{{ j+i }}</span></div>`,
@@ -124,12 +117,38 @@ describe('compiler: v-for', () => {
   })
 
   test('object de-structured value', () => {
+    const { code, ir } = compileWithVFor(
+      '<span v-for="({ id, value }) in items" :key="id">{{ id }}{{ value }}</span>',
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.operation[0]).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'items',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '{ id, value }',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ObjectPattern',
+            },
+          ],
+        },
+      },
+      key: undefined,
+      index: undefined,
+    })
+  })
+
+  test.todo('object de-structured value (with rest)', () => {
     const { code, ir } = compileWithVFor(
       `<div v-for="(  { id, ...other }, index) in list" :key="id">{{ id + other + index }}</div>`,
     )
     expect(code).matchSnapshot()
-    expect(code).contains(`([{ id, ...other }, index]) => [id, other, index]`)
-    expect(code).contains(`_ctx0[0] + _ctx0[1] + _ctx0[2]`)
     expect(ir.block.operation[0]).toMatchObject({
       type: IRNodeTypes.FOR,
       source: {
@@ -157,12 +176,41 @@ describe('compiler: v-for', () => {
   })
 
   test('array de-structured value', () => {
+    const { code, ir } = compileWithVFor(
+      `<div v-for="([id, other], index) in list" :key="id">{{ id + other + index }}</div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(ir.block.operation[0]).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '[id, other]',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ArrayPattern',
+            },
+          ],
+        },
+      },
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'index',
+      },
+      index: undefined,
+    })
+  })
+
+  test.todo('array de-structured value (with rest)', () => {
     const { code, ir } = compileWithVFor(
       `<div v-for="([id, ...other], index) in list" :key="id">{{ id + other + index }}</div>`,
     )
     expect(code).matchSnapshot()
-    expect(code).contains(`([[id, ...other], index]) => [id, other, index]`)
-    expect(code).contains(`_ctx0[0] + _ctx0[1] + _ctx0[2]`)
     expect(ir.block.operation[0]).toMatchObject({
       type: IRNodeTypes.FOR,
       source: {
@@ -189,7 +237,7 @@ describe('compiler: v-for', () => {
     })
   })
 
-  test('v-for aliases w/ complex expressions', () => {
+  test.todo('v-for aliases w/ complex expressions', () => {
     const { code, ir } = compileWithVFor(
       `<div v-for="({ foo = bar, baz: [qux = quux] }) in list">
         {{ foo + bar + baz + qux + quux }}
@@ -222,17 +270,4 @@ describe('compiler: v-for', () => {
       index: undefined,
     })
   })
-
-  test('function params w/ prefixIdentifiers: false', () => {
-    const { code } = compileWithVFor(
-      `<div v-for="(item, , k) of items" :key="k">{{ item }}</div>`,
-      {
-        prefixIdentifiers: false,
-      },
-    )
-
-    expect(code).contains(`_createFor(() => (items), ([item, __, k]) => {`)
-    expect(code).contain(`_setText(n2, item)`)
-    expect(code).matchSnapshot()
-  })
 })
index d061ec0dcda74390a4b5027ee7bf0c2ff3374925..9e42789958f19adbfe3cc5797da4b04afd5eda8a 100644 (file)
@@ -3,56 +3,39 @@ import { genBlock } from './block'
 import { genExpression } from './expression'
 import type { CodegenContext } from '../generate'
 import type { ForIRNode } from '../ir'
-import {
-  type CodeFragment,
-  DELIMITERS_ARRAY,
-  NEWLINE,
-  genCall,
-  genMulti,
-} from './utils'
+import { type CodeFragment, NEWLINE, genCall, genMulti } from './utils'
+import type { Identifier } from '@babel/types'
 
 export function genFor(
   oper: ForIRNode,
   context: CodegenContext,
 ): CodeFragment[] {
   const { helper } = context
-  const { source, value, key, index, render, keyProp, once, id, container } =
+  const { source, value, key, index, render, keyProp, once, id, component } =
     oper
 
-  let isDestructure = false
   let rawValue: string | null = null
   const rawKey = key && key.content
   const rawIndex = index && index.content
 
   const sourceExpr = ['() => (', ...genExpression(source, context), ')']
-  const idsInValue = getIdsInValue()
-  let blockFn = genBlockFn()
-  const simpleIdMap: Record<string, null> = genSimpleIdMap()
+  const idToPathMap = parseValueDestructure()
 
-  if (isDestructure) {
-    const idMap: Record<string, null> = {}
-    idsInValue.forEach(id => (idMap[id] = null))
-    if (rawKey) idMap[rawKey] = null
-    if (rawIndex) idMap[rawIndex] = null
-    const destructureAssignmentFn: CodeFragment[] = [
-      '(',
-      ...genMulti(
-        DELIMITERS_ARRAY,
-        rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
-        rawKey ? rawKey : rawIndex ? '__' : undefined,
-        rawIndex,
-      ),
-      ') => ',
-      ...genMulti(DELIMITERS_ARRAY, ...idsInValue, rawKey, rawIndex),
-    ]
+  const [depth, exitScope] = context.enterScope()
+  const propsName = `_ctx${depth}`
+  const idMap: Record<string, string | null> = {}
 
-    blockFn = genCall(
-      // @ts-expect-error
-      helper('withDestructure'),
-      destructureAssignmentFn,
-      blockFn,
-    )
-  }
+  idToPathMap.forEach((path, id) => {
+    idMap[id] = `${propsName}[0].value${path}`
+  })
+  if (rawKey) idMap[rawKey] = `${propsName}[1].value`
+  if (rawIndex) idMap[rawIndex] = `${propsName}[2].value`
+
+  const blockFn = context.withId(
+    () => genBlock(render, context, [propsName]),
+    idMap,
+  )
+  exitScope()
 
   return [
     NEWLINE,
@@ -62,67 +45,68 @@ export function genFor(
       sourceExpr,
       blockFn,
       genCallback(keyProp),
-      container != null && `n${container}`,
-      false, // todo: hydrationNode
+      component && 'true',
       once && 'true',
+      // todo: hydrationNode
     ),
   ]
 
-  function getIdsInValue() {
-    const idsInValue = new Set<string>()
+  // construct a id -> accessor path map.
+  // e.g. `{ x: { y: [z] }}` -> `Map{ 'z' => '.x.y[0]' }`
+  function parseValueDestructure() {
+    const map = new Map<string, string>()
     if (value) {
       rawValue = value && value.content
-      if ((isDestructure = !!value.ast)) {
+      if (value.ast) {
         walkIdentifiers(
           value.ast,
-          (id, _, __, ___, isLocal) => {
-            if (isLocal) idsInValue.add(id.name)
+          (id, _, parentStack, ___, isLocal) => {
+            if (isLocal) {
+              let path = ''
+              for (let i = 0; i < parentStack.length; i++) {
+                const parent = parentStack[i]
+                const child = parentStack[i + 1] || id
+                if (
+                  parent.type === 'ObjectProperty' &&
+                  parent.value === child
+                ) {
+                  if (parent.computed && parent.key.type !== 'StringLiteral') {
+                    // TODO need to process this
+                    path += `[${value.content.slice(
+                      parent.key.start!,
+                      parent.key.end!,
+                    )}]`
+                  } else if (parent.key.type === 'StringLiteral') {
+                    path += `[${JSON.stringify(parent.key.value)}]`
+                  } else {
+                    // non-computed, can only be identifier
+                    path += `.${(parent.key as Identifier).name}`
+                  }
+                } else if (parent.type === 'ArrayPattern') {
+                  const index = parent.elements.indexOf(child as any)
+                  path += `[${index}]`
+                }
+                // TODO handle rest spread
+              }
+              map.set(id.name, path)
+            }
           },
           true,
         )
       } else {
-        idsInValue.add(rawValue)
+        map.set(rawValue, '')
       }
     }
-    return idsInValue
-  }
-
-  function genBlockFn() {
-    const [depth, exitScope] = context.enterScope()
-    let propsName: string
-    const idMap: Record<string, string | null> = {}
-    if (context.options.prefixIdentifiers) {
-      propsName = `_ctx${depth}`
-      let suffix = isDestructure ? '' : '.value'
-      Array.from(idsInValue).forEach(
-        (id, idIndex) => (idMap[id] = `${propsName}[${idIndex}]${suffix}`),
-      )
-      if (rawKey) idMap[rawKey] = `${propsName}[${idsInValue.size}]${suffix}`
-      if (rawIndex)
-        idMap[rawIndex] = `${propsName}[${idsInValue.size + 1}]${suffix}`
-    } else {
-      propsName = `[${[rawValue || ((rawKey || rawIndex) && '_'), rawKey || (rawIndex && '__'), rawIndex].filter(Boolean).join(', ')}]`
-    }
-
-    const blockFn = context.withId(
-      () => genBlock(render, context, [propsName]),
-      idMap,
-    )
-    exitScope()
-    return blockFn
-  }
-
-  function genSimpleIdMap() {
-    const idMap: Record<string, null> = {}
-    if (rawKey) idMap[rawKey] = null
-    if (rawIndex) idMap[rawIndex] = null
-    idsInValue.forEach(id => (idMap[id] = null))
-    return idMap
+    return map
   }
 
+  // TODO this should be looked at for destructure cases
   function genCallback(expr: SimpleExpressionNode | undefined) {
     if (!expr) return false
-    const res = context.withId(() => genExpression(expr, context), simpleIdMap)
+    const res = context.withId(
+      () => genExpression(expr, context),
+      genSimpleIdMap(),
+    )
     return [
       ...genMulti(
         ['(', ')', ', '],
@@ -135,4 +119,12 @@ export function genFor(
       ')',
     ]
   }
+
+  function genSimpleIdMap() {
+    const idMap: Record<string, null> = {}
+    if (rawKey) idMap[rawKey] = null
+    if (rawIndex) idMap[rawIndex] = null
+    idToPathMap.forEach((_, id) => (idMap[id] = null))
+    return idMap
+  }
 }
index 05d7b51d278d433418aba74a5764b92e4caa3992..3af8af961d7d5210d975219c8399c6932641a1d6 100644 (file)
@@ -90,7 +90,7 @@ export interface ForIRNode extends BaseIRNode, IRFor {
   keyProp?: SimpleExpressionNode
   render: BlockIRNode
   once: boolean
-  container?: number
+  component: boolean
 }
 
 export interface SetPropIRNode extends BaseIRNode {
index 020822478aea77fe2ec8d462e13a2ddfdefc1bd0..7b477882235c1ab94c2dd53f7e38f67097260825 100644 (file)
@@ -1,5 +1,6 @@
 import {
   type ElementNode,
+  ElementTypes,
   ErrorCodes,
   type SimpleExpressionNode,
   createCompilerError,
@@ -46,6 +47,7 @@ export function processFor(
 
   const keyProp = findProp(node, 'key')
   const keyProperty = keyProp && propToExpression(keyProp)
+  const isComponent = node.tagType === ElementTypes.COMPONENT
   context.node = node = wrapTemplate(node, ['for'])
   context.dynamic.flags |= DynamicFlag.NON_TEMPLATE | DynamicFlag.INSERT
   const id = context.reference()
@@ -55,15 +57,6 @@ export function processFor(
 
   return (): void => {
     exitBlock()
-    const { parent } = context
-    let container: number | undefined
-    if (
-      parent &&
-      parent.block.node !== parent.node &&
-      parent.node.children.length === 1
-    ) {
-      container = parent.reference()
-    }
     context.registerOperation({
       type: IRNodeTypes.FOR,
       id,
@@ -74,7 +67,7 @@ export function processFor(
       keyProp: keyProperty,
       render,
       once: context.inVOnce,
-      container,
+      component: isComponent,
     })
   }
 }