]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-vapor): enhance v-slot prop destructuring support (#14165)
authoredison <daiwei521@126.com>
Thu, 4 Dec 2025 00:33:57 +0000 (08:33 +0800)
committerGitHub <noreply@github.com>
Thu, 4 Dec 2025 00:33:57 +0000 (08:33 +0800)
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap
packages/compiler-vapor/__tests__/__snapshots__/scopeId.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/generators/for.ts

index a5d422aed2a189d02727d54e3195a810b3c6d55b..cb7b5de8683f12250e19c6777ec70d3dca8e52cb 100644 (file)
@@ -324,7 +324,7 @@ export function render(_ctx) {
   const n1 = _createComponentWithFallback(_component_Comp, null, {
     "foo": _withVaporCtx((_slotProps0) => {
       const n0 = t0()
-      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["a"] + _slotProps0["b"])))
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.a + _slotProps0.b)))
       return n0
     })
   }, true)
index 3d0322ce519fc5ea489b6a926936312b91a50c6a..c49073973a1e97fa807a35133bd63ddbe3eb1d19 100644 (file)
@@ -56,7 +56,7 @@ export function render(_ctx) {
   const n4 = _createComponentWithFallback(_component_Child, null, {
     "foo": _withVaporCtx((_slotProps0) => {
       const n0 = t0()
-      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["msg"])))
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.msg)))
       return n0
     }),
     "bar": _withVaporCtx(() => {
index e3251a687d9a29e784c25b99bd32642ee0516219..95c90354c6aa99ed047dcfdfe811dc372a89fa9d 100644 (file)
@@ -33,7 +33,7 @@ export function render(_ctx) {
         name: item,
         fn: _withVaporCtx((_slotProps0) => {
           const n0 = t0()
-          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["bar"])))
+          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.bar)))
           return n0
         })
       })))
@@ -275,12 +275,12 @@ export function render(_ctx) {
       const n1 = _createComponentWithFallback(_component_Inner, null, {
         "default": _withVaporCtx((_slotProps1) => {
           const n0 = t0()
-          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz)))
+          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.foo + _slotProps1.bar + _ctx.baz)))
           return n0
         })
       })
       const n3 = t0()
-      _renderEffect(() => _setText(n3, " " + _toDisplayString(_slotProps0["foo"] + _ctx.bar + _ctx.baz)))
+      _renderEffect(() => _setText(n3, " " + _toDisplayString(_slotProps0.foo + _ctx.bar + _ctx.baz)))
       return [n1, n3]
     })
   }, true)
@@ -300,7 +300,7 @@ export function render(_ctx) {
         name: _ctx.named,
         fn: _withVaporCtx((_slotProps0) => {
           const n0 = t0()
-          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
+          _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.foo + _ctx.bar)))
           return n0
         })
       })
@@ -319,7 +319,7 @@ export function render(_ctx) {
   const n1 = _createComponentWithFallback(_component_Comp, null, {
     "named": _withVaporCtx((_slotProps0) => {
       const n0 = t0()
-      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.foo + _ctx.bar)))
       return n0
     })
   }, true)
@@ -336,7 +336,7 @@ export function render(_ctx) {
   const n1 = _createComponentWithFallback(_component_Comp, null, {
     "default": _withVaporCtx((_slotProps0) => {
       const n0 = t0()
-      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0["foo"] + _ctx.bar)))
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.foo + _ctx.bar)))
       return n0
     })
   }, true)
@@ -380,6 +380,142 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: transform slot > slot prop alias uses original key 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.msg)))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop array rest destructuring 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.arr.slice(1)[0])))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop computed key destructuring 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0[_ctx.key])))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop default value 1`] = `
+"import { resolveComponent as _resolveComponent, getDefaultValue as _getDefaultValue, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_getDefaultValue(_slotProps0.foo, 1))))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop nested default value 1`] = `
+"import { resolveComponent as _resolveComponent, getDefaultValue as _getDefaultValue, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_getDefaultValue(_slotProps0.foo[0], 1) + _getDefaultValue(_slotProps0.baz.qux, 2))))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop nested destructuring 1`] = `
+"import { resolveComponent as _resolveComponent, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.foo.bar)))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop rest destructuring 1`] = `
+"import { resolveComponent as _resolveComponent, getRestElement as _getRestElement, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_getRestElement(_slotProps0, ["foo"]).bar)))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > slot prop rest with computed keys preserved 1`] = `
+"import { resolveComponent as _resolveComponent, getRestElement as _getRestElement, toDisplayString as _toDisplayString, setText as _setText, renderEffect as _renderEffect, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
+const t0 = _template(" ")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": _withVaporCtx((_slotProps0) => {
+      const n0 = t0()
+      _renderEffect(() => _setText(n0, _toDisplayString(_slotProps0.foo + _getRestElement(_slotProps0, ["foo", _ctx.key]).other)))
+      return n0
+    })
+  }, true)
+  return n2
+}"
+`;
+
 exports[`compiler: transform slot > with whitespace: 'preserve' > implicit default slot 1`] = `
 "import { resolveComponent as _resolveComponent, withVaporCtx as _withVaporCtx, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
 const t0 = _template(" Header ")
index 093f1d577be87f7346cb22bbfbec9d53516b04af..b70ee26970b85d9bfa6d671bc8581a4ba106ce2a 100644 (file)
@@ -68,7 +68,7 @@ describe('compiler: transform slot', () => {
     expect(code).toMatchSnapshot()
 
     expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
-    expect(code).contains(`_slotProps0["foo"] + _ctx.bar`)
+    expect(code).contains(`_slotProps0.foo + _ctx.bar`)
 
     expect(ir.block.dynamic.children[0].operation).toMatchObject({
       type: IRNodeTypes.CREATE_COMPONENT_NODE,
@@ -102,7 +102,7 @@ describe('compiler: transform slot', () => {
     expect(code).toMatchSnapshot()
 
     expect(code).contains(`"named": _withVaporCtx((_slotProps0) =>`)
-    expect(code).contains(`_slotProps0["foo"] + _ctx.bar`)
+    expect(code).contains(`_slotProps0.foo + _ctx.bar`)
 
     expect(ir.block.dynamic.children[0].operation).toMatchObject({
       type: IRNodeTypes.CREATE_COMPONENT_NODE,
@@ -131,7 +131,7 @@ describe('compiler: transform slot', () => {
     expect(code).toMatchSnapshot()
 
     expect(code).contains(`fn: _withVaporCtx((_slotProps0) =>`)
-    expect(code).contains(`_slotProps0["foo"] + _ctx.bar`)
+    expect(code).contains(`_slotProps0.foo + _ctx.bar`)
 
     expect(ir.block.dynamic.children[0].operation).toMatchObject({
       type: IRNodeTypes.CREATE_COMPONENT_NODE,
@@ -165,6 +165,87 @@ describe('compiler: transform slot', () => {
     expect(code).toMatchSnapshot()
   })
 
+  test('slot prop alias uses original key', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ msg: msg1 }">{{ msg1 }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_slotProps0.msg`)
+  })
+
+  test('slot prop nested destructuring', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ foo: { bar: baz } }">{{ baz }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_slotProps0.foo.bar`)
+  })
+
+  test('slot prop computed key destructuring', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ [key]: val }">{{ val }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_slotProps0[_ctx.key]`)
+  })
+
+  test('slot prop rest destructuring', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ foo, ...rest }">{{ rest.bar }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_getRestElement(_slotProps0`)
+  })
+
+  test('slot prop array rest destructuring', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ arr: [first, ...rest] }">{{ rest[0] }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_slotProps0.arr.slice(1)`)
+  })
+
+  test('slot prop default value', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ foo = 1 }">{{ foo }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_getDefaultValue(_slotProps0.foo, 1)`)
+  })
+
+  test('slot prop nested default value', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ foo: [bar = 1], baz: { qux = 2 } }">{{ bar + qux }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_getDefaultValue(_slotProps0.foo[0], 1)`)
+    expect(code).contains(`_getDefaultValue(_slotProps0.baz.qux, 2)`)
+  })
+
+  test('slot prop rest with computed keys preserved', () => {
+    const { code } = compileWithSlots(
+      `<Comp><template #default="{ foo, [key]: val, ...rest }">{{ foo + rest.other }}</template></Comp>`,
+    )
+
+    expect(code).toMatchSnapshot()
+    expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
+    expect(code).contains(`_getRestElement(_slotProps0, ["foo", _ctx.key])`)
+  })
+
   test('named slots w/ implicit default slot', () => {
     const { ir, code } = compileWithSlots(
       `<Comp>
@@ -216,8 +297,8 @@ describe('compiler: transform slot', () => {
 
     expect(code).contains(`"default": _withVaporCtx((_slotProps0) =>`)
     expect(code).contains(`"default": _withVaporCtx((_slotProps1) =>`)
-    expect(code).contains(`_slotProps0["foo"] + _slotProps1["bar"] + _ctx.baz`)
-    expect(code).contains(`_slotProps0["foo"] + _ctx.bar + _ctx.baz`)
+    expect(code).contains(`_slotProps0.foo + _slotProps1.bar + _ctx.baz`)
+    expect(code).contains(`_slotProps0.foo + _ctx.bar + _ctx.baz`)
 
     const outerOp = ir.block.dynamic.children[0].operation
     expect(outerOp).toMatchObject({
@@ -293,7 +374,7 @@ describe('compiler: transform slot', () => {
     expect(code).toMatchSnapshot()
 
     expect(code).contains(`fn: _withVaporCtx((_slotProps0) =>`)
-    expect(code).contains(`_setText(n0, _toDisplayString(_slotProps0["bar"]))`)
+    expect(code).contains(`_setText(n0, _toDisplayString(_slotProps0.bar))`)
 
     expect(ir.block.dynamic.children[0].operation).toMatchObject({
       type: IRNodeTypes.CREATE_COMPONENT_NODE,
index b1fe3612c1162bf4f890b0d61c2d71727d92647c..c4e9061345d012e8ad9d82907767b8dd88ec9846 100644 (file)
@@ -34,11 +34,16 @@ import {
   createSimpleExpression,
   isMemberExpression,
   toValidAssetId,
-  walkIdentifiers,
 } from '@vue/compiler-dom'
 import { genEventHandler } from './event'
 import { genDirectiveModifiers, genDirectivesForElement } from './directive'
 import { genBlock } from './block'
+import {
+  type DestructureMap,
+  type DestructureMapValue,
+  buildDestructureIdMap,
+  parseValueDestructure,
+} from './for'
 import { genModelHandler } from './vModel'
 import { isBuiltInComponent, isKeepAliveTag } from '../utils'
 
@@ -405,40 +410,37 @@ function genConditionalSlot(
 }
 
 function genSlotBlockWithProps(oper: SlotBlockIRNode, context: CodegenContext) {
-  let isDestructureAssignment = false
-  let rawProps: string | undefined
   let propsName: string | undefined
   let exitScope: (() => void) | undefined
   let depth: number | undefined
   const { props, key, node } = oper
-  const idsOfProps = new Set<string>()
+  const idToPathMap: DestructureMap = props
+    ? parseValueDestructure(props, context)
+    : new Map<string, DestructureMapValue | null>()
+
   if (props) {
-    rawProps = props.content
-    if ((isDestructureAssignment = !!props.ast)) {
+    if (props.ast) {
       ;[depth, exitScope] = context.enterScope()
       propsName = `_slotProps${depth}`
-      walkIdentifiers(
-        props.ast,
-        (id, _, __, ___, isLocal) => {
-          if (isLocal) idsOfProps.add(id.name)
-        },
-        true,
-      )
     } else {
-      idsOfProps.add((propsName = rawProps))
+      propsName = props.content
     }
   }
 
-  const idMap: Record<string, string | null> = {}
+  const idMap = idToPathMap.size
+    ? buildDestructureIdMap(
+        idToPathMap,
+        propsName || '',
+        context.options.expressionPlugins,
+      )
+    : {}
+
+  if (propsName) {
+    idMap[propsName] = null
+  }
 
-  idsOfProps.forEach(
-    id =>
-      (idMap[id] = isDestructureAssignment
-        ? `${propsName}[${JSON.stringify(id)}]`
-        : null),
-  )
   let blockFn = context.withId(
-    () => genBlock(oper, context, [propsName]),
+    () => genBlock(oper, context, propsName ? [propsName] : []),
     idMap,
   )
   exitScope && exitScope()
index ddb5d3b43a891477f128700f05c6ec80ade52832..2ab9d5f655a6d489b432f6abd5e7b5120bbc86e0 100644 (file)
@@ -40,40 +40,22 @@ export function genFor(
     onlyChild,
   } = oper
 
-  let rawValue: string | null = null
+  const rawValue = value && value.content
   const rawKey = key && key.content
   const rawIndex = index && index.content
 
   const sourceExpr = ['() => (', ...genExpression(source, context), ')']
-  const idToPathMap = parseValueDestructure()
+  const idToPathMap = parseValueDestructure(value, context)
 
   const [depth, exitScope] = context.enterScope()
-  const idMap: Record<string, string | SimpleExpressionNode | null> = {}
-
   const itemVar = `_for_item${depth}`
+  const idMap = buildDestructureIdMap(
+    idToPathMap,
+    `${itemVar}.value`,
+    context.options.expressionPlugins,
+  )
   idMap[itemVar] = null
 
-  idToPathMap.forEach((pathInfo, id) => {
-    let path = `${itemVar}.value${pathInfo ? pathInfo.path : ''}`
-    if (pathInfo) {
-      if (pathInfo.helper) {
-        idMap[pathInfo.helper] = null
-        path = `${pathInfo.helper}(${path}, ${pathInfo.helperArgs})`
-      }
-      if (pathInfo.dynamic) {
-        const node = (idMap[id] = createSimpleExpression(path))
-        const plugins = context.options.expressionPlugins
-        node.ast = parseExpression(`(${path})`, {
-          plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
-        })
-      } else {
-        idMap[id] = path
-      }
-    } else {
-      idMap[id] = path
-    }
-  })
-
   const args = [itemVar]
   if (rawKey) {
     const keyVar = `_for_key${depth}`
@@ -180,108 +162,6 @@ export function genFor(
     ),
   ]
 
-  // construct a id -> accessor path map.
-  // e.g. `{ x: { y: [z] }}` -> `Map{ 'z' => '.x.y[0]' }`
-  function parseValueDestructure() {
-    const map = new Map<
-      string,
-      {
-        path: string
-        dynamic: boolean
-        helper?: string
-        helperArgs?: string
-      } | null
-    >()
-    if (value) {
-      rawValue = value && value.content
-      if (value.ast) {
-        walkIdentifiers(
-          value.ast,
-          (id, _, parentStack, ___, isLocal) => {
-            if (isLocal) {
-              let path = ''
-              let isDynamic = false
-              let helper
-              let helperArgs
-              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.key.type === 'StringLiteral') {
-                    path += `[${JSON.stringify(parent.key.value)}]`
-                  } else if (parent.computed) {
-                    isDynamic = true
-                    path += `[${value.content.slice(
-                      parent.key.start! - 1,
-                      parent.key.end! - 1,
-                    )}]`
-                  } 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)
-                  if (child.type === 'RestElement') {
-                    path += `.slice(${index})`
-                  } else {
-                    path += `[${index}]`
-                  }
-                } else if (
-                  parent.type === 'ObjectPattern' &&
-                  child.type === 'RestElement'
-                ) {
-                  helper = context.helper('getRestElement')
-                  helperArgs =
-                    '[' +
-                    parent.properties
-                      .filter(p => p.type === 'ObjectProperty')
-                      .map(p => {
-                        if (p.key.type === 'StringLiteral') {
-                          return JSON.stringify(p.key.value)
-                        } else if (p.computed) {
-                          isDynamic = true
-                          return value.content.slice(
-                            p.key.start! - 1,
-                            p.key.end! - 1,
-                          )
-                        } else {
-                          return JSON.stringify((p.key as Identifier).name)
-                        }
-                      })
-                      .join(', ') +
-                    ']'
-                }
-
-                // default value
-                if (
-                  child.type === 'AssignmentPattern' &&
-                  (parent.type === 'ObjectProperty' ||
-                    parent.type === 'ArrayPattern')
-                ) {
-                  isDynamic = true
-                  helper = context.helper('getDefaultValue')
-                  helperArgs = value.content.slice(
-                    child.right.start! - 1,
-                    child.right.end! - 1,
-                  )
-                }
-              }
-              map.set(id.name, { path, dynamic: isDynamic, helper, helperArgs })
-            }
-          },
-          true,
-        )
-      } else {
-        map.set(rawValue, null)
-      }
-    }
-    return map
-  }
-
   function genCallback(expr: SimpleExpressionNode | undefined) {
     if (!expr) return false
     const res = context.withId(
@@ -310,6 +190,139 @@ export function genFor(
   }
 }
 
+export type DestructureMapValue = {
+  path: string
+  dynamic: boolean
+  helper?: string
+  helperArgs?: string
+}
+
+export type DestructureMap = Map<string, DestructureMapValue | null>
+
+// construct a id -> accessor path map.
+// e.g. `{ x: { y: [z] }}` -> `Map{ 'z' => '.x.y[0]' }`
+export function parseValueDestructure(
+  value: SimpleExpressionNode | undefined,
+  context: CodegenContext,
+): DestructureMap {
+  const map: DestructureMap = new Map()
+  if (value) {
+    const rawValue = value.content
+    if (value.ast) {
+      walkIdentifiers(
+        value.ast,
+        (id, _, parentStack, ___, isLocal) => {
+          if (isLocal) {
+            let path = ''
+            let isDynamic = false
+            let helper
+            let helperArgs
+            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.key.type === 'StringLiteral') {
+                  path += `[${JSON.stringify(parent.key.value)}]`
+                } else if (parent.computed) {
+                  isDynamic = true
+                  path += `[${rawValue.slice(
+                    parent.key.start! - 1,
+                    parent.key.end! - 1,
+                  )}]`
+                } 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)
+                if (child.type === 'RestElement') {
+                  path += `.slice(${index})`
+                } else {
+                  path += `[${index}]`
+                }
+              } else if (
+                parent.type === 'ObjectPattern' &&
+                child.type === 'RestElement'
+              ) {
+                helper = context.helper('getRestElement')
+                helperArgs =
+                  '[' +
+                  parent.properties
+                    .filter(p => p.type === 'ObjectProperty')
+                    .map(p => {
+                      if (p.key.type === 'StringLiteral') {
+                        return JSON.stringify(p.key.value)
+                      } else if (p.computed) {
+                        isDynamic = true
+                        return rawValue.slice(p.key.start! - 1, p.key.end! - 1)
+                      } else {
+                        return JSON.stringify((p.key as Identifier).name)
+                      }
+                    })
+                    .join(', ') +
+                  ']'
+              }
+
+              // default value
+              if (
+                child.type === 'AssignmentPattern' &&
+                (parent.type === 'ObjectProperty' ||
+                  parent.type === 'ArrayPattern')
+              ) {
+                isDynamic = true
+                helper = context.helper('getDefaultValue')
+                helperArgs = rawValue.slice(
+                  child.right.start! - 1,
+                  child.right.end! - 1,
+                )
+              }
+            }
+            map.set(id.name, { path, dynamic: isDynamic, helper, helperArgs })
+          }
+        },
+        true,
+      )
+    } else if (rawValue) {
+      map.set(rawValue, null)
+    }
+  }
+  return map
+}
+
+export function buildDestructureIdMap(
+  idToPathMap: DestructureMap,
+  baseAccessor: string,
+  plugins: CodegenContext['options']['expressionPlugins'],
+): Record<string, string | SimpleExpressionNode | null> {
+  const idMap: Record<string, string | SimpleExpressionNode | null> = {}
+  idToPathMap.forEach((pathInfo, id) => {
+    let path = baseAccessor
+    if (pathInfo) {
+      path = `${baseAccessor}${pathInfo.path}`
+
+      if (pathInfo.helper) {
+        idMap[pathInfo.helper] = null
+        path = pathInfo.helperArgs
+          ? `${pathInfo.helper}(${path}, ${pathInfo.helperArgs})`
+          : `${pathInfo.helper}(${path})`
+      }
+
+      if (pathInfo.dynamic) {
+        const node = (idMap[id] = createSimpleExpression(path))
+        node.ast = parseExpression(`(${path})`, {
+          plugins: plugins ? [...plugins, 'typescript'] : ['typescript'],
+        })
+      } else {
+        idMap[id] = path
+      }
+    } else {
+      idMap[id] = path
+    }
+  })
+  return idMap
+}
+
 function matchPatterns(
   render: BlockIRNode,
   keyProp: SimpleExpressionNode | undefined,