Просмотр исходного кода

feat: destructuring + nesting in v-for (#217)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Rizumu Ayaka 2 лет назад
Родитель
Сommit
868c4294a0

+ 2 - 2
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap

@@ -17,10 +17,10 @@ exports[`compiler: template ref transform > ref + v-for 1`] = `
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
-  const n0 = _createFor(() => ([1,2,3]), (_block) => {
+  const n0 = _createFor(() => ([1,2,3]), (_ctx0) => {
     const n2 = t0()
     _setRef(n2, "foo", void 0, true)
-    return [n2, () => {}]
+    return n2
   })
   return n0
 }"

+ 83 - 20
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap

@@ -1,39 +1,102 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
+exports[`compiler: v-for > array de-structured value 1`] = `
+"import { renderEffect as _renderEffect, setText as _setText, createFor as _createFor, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
+    const n2 = t0()
+    _renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]))
+    return n2
+  }, ([id, ...other], index) => (id), null, null, false, _state => {
+    const [[id, ...other], index] = _state
+    return [id, other, index]
+  })
+  return n0
+}"
+`;
+
 exports[`compiler: v-for > basic v-for 1`] = `
-"import { delegate as _delegate, setText as _setText, renderEffect as _renderEffect, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
+"import { delegate as _delegate, renderEffect as _renderEffect, setText as _setText, createFor as _createFor, delegateEvents as _delegateEvents, template as _template } from 'vue/vapor';
 const t0 = _template("<div></div>")
 _delegateEvents("click")
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.items), (_block) => {
+  const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
     const n2 = t0()
-    _delegate(n2, "click", () => $event => (_ctx.remove(_block.s[0])))
-    const _updateEffect = () => {
-      const [item] = _block.s
-      _setText(n2, item)
-    }
-    _renderEffect(_updateEffect)
-    return [n2, _updateEffect]
+    _delegate(n2, "click", () => $event => (_ctx.remove(_ctx0[0])))
+    _renderEffect(() => _setText(n2, _ctx0[0]))
+    return n2
   }, (item) => (item.id))
   return n0
 }"
 `;
 
 exports[`compiler: v-for > multi effect 1`] = `
-"import { setDynamicProp as _setDynamicProp, renderEffect as _renderEffect, createFor as _createFor, template as _template } from 'vue/vapor';
+"import { renderEffect as _renderEffect, setDynamicProp as _setDynamicProp, createFor as _createFor, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
+    const n2 = t0()
+    _renderEffect(() => _setDynamicProp(n2, "item", _ctx0[0]))
+    _renderEffect(() => _setDynamicProp(n2, "index", _ctx0[1]))
+    return n2
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > nested v-for 1`] = `
+"import { renderEffect as _renderEffect, setText as _setText, createFor as _createFor, insert as _insert, template as _template } from 'vue/vapor';
+const t0 = _template("<span></span>")
+const t1 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
+    const n5 = t1()
+    const n2 = _createFor(() => (_ctx0[0]), (_ctx2) => {
+      const n4 = t0()
+      _renderEffect(() => _setText(n4, _ctx2[0]+_ctx0[0]))
+      return n4
+    })
+    _insert(n2, n5)
+    return n5
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > object de-structured value 1`] = `
+"import { renderEffect as _renderEffect, setText as _setText, createFor as _createFor, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
+    const n2 = t0()
+    _renderEffect(() => _setText(n2, _ctx0[0] + _ctx0[1] + _ctx0[2]))
+    return n2
+  }, ({ id, ...other }, index) => (id), null, null, false, _state => {
+    const [{ id, ...other }, index] = _state
+    return [id, other, index]
+  })
+  return n0
+}"
+`;
+
+exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = `
+"import { renderEffect as _renderEffect, setText as _setText, createFor as _createFor, template as _template } from 'vue/vapor';
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.items), (_block) => {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
     const n2 = t0()
-    const _updateEffect = () => {
-      const [item, index] = _block.s
-      _setDynamicProp(n2, "item", item)
-      _setDynamicProp(n2, "index", index)
-    }
-    _renderEffect(_updateEffect)
-    return [n2, _updateEffect]
+    _renderEffect(() => _setText(n2, _ctx0[0] + _ctx.bar + _ctx.baz + _ctx0[1] + _ctx.quux))
+    return n2
+  }, null, null, null, false, _state => {
+    const [{ foo = bar, baz: [qux = quux] }] = _state
+    return [foo, qux]
   })
   return n0
 }"
@@ -44,9 +107,9 @@ exports[`compiler: v-for > w/o value 1`] = `
 const t0 = _template("<div>item</div>")
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.items), (_block) => {
+  const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
     const n2 = t0()
-    return [n2, () => {}]
+    return n2
   })
   return n0
 }"

+ 2 - 2
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap

@@ -67,9 +67,9 @@ exports[`compiler: v-once > with v-for 1`] = `
 const t0 = _template("<div></div>")
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.list), (_block) => {
+  const n0 = _createFor(() => (_ctx.list), (_ctx0) => {
     const n2 = t0()
-    return [n2, () => {}]
+    return n2
   }, null, null, null, true)
   return n0
 }"

+ 137 - 0
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts

@@ -86,4 +86,141 @@ describe('compiler: v-for', () => {
     )
     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>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains(`_createFor(() => (_ctx.list), (_ctx0) => {`)
+    expect(code).contains(`_createFor(() => (_ctx0[0]), (_ctx2) => {`)
+    expect(code).contains(`_ctx2[0]+_ctx0[0]`)
+    expect(ir.template).toEqual(['<span></span>', '<div></div>'])
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.FOR,
+        id: 0,
+        source: { content: 'list' },
+        value: { content: 'i' },
+        render: {
+          type: IRNodeTypes.BLOCK,
+          dynamic: {
+            children: [{ template: 1 }],
+          },
+        },
+      },
+    ])
+    expect((ir.block.operation[0] as any).render.operation[0]).toMatchObject({
+      type: IRNodeTypes.FOR,
+      id: 2,
+      source: { content: 'i' },
+      value: { content: 'j' },
+      render: {
+        type: IRNodeTypes.BLOCK,
+        dynamic: {
+          children: [{ template: 0 }],
+        },
+      },
+    })
+  })
+
+  test('object 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(code).contains(`return [id, other, index]`)
+    expect(code).contains(`_ctx0[0] + _ctx0[1] + _ctx0[2]`)
+    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: 'ObjectPattern',
+            },
+          ],
+        },
+      },
+      key: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'index',
+      },
+      index: undefined,
+    })
+  })
+
+  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(code).contains(`return [id, other, index]`)
+    expect(code).contains(`_ctx0[0] + _ctx0[1] + _ctx0[2]`)
+    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('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 }}
+      </div>`,
+    )
+    expect(code).matchSnapshot()
+    expect(code).contains(`return [foo, qux]`)
+    expect(code).contains(
+      `_ctx0[0] + _ctx.bar + _ctx.baz + _ctx0[1] + _ctx.quux`,
+    )
+    expect(ir.block.operation[0]).toMatchObject({
+      type: IRNodeTypes.FOR,
+      source: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: 'list',
+      },
+      value: {
+        type: NodeTypes.SIMPLE_EXPRESSION,
+        content: '{ foo = bar, baz: [qux = quux] }',
+        ast: {
+          type: 'ArrowFunctionExpression',
+          params: [
+            {
+              type: 'ObjectPattern',
+            },
+          ],
+        },
+      },
+      key: undefined,
+      index: undefined,
+    })
+  })
 })

+ 1 - 5
packages/compiler-vapor/src/generate.ts

@@ -2,7 +2,7 @@ import type {
   CodegenOptions as BaseCodegenOptions,
   BaseCodegenResult,
 } from '@vue/compiler-dom'
-import type { BlockIRNode, IREffect, RootIRNode, VaporHelper } from './ir'
+import type { BlockIRNode, RootIRNode, VaporHelper } from './ir'
 import { extend, remove } from '@vue/shared'
 import { genBlockContent } from './generators/block'
 import { genTemplates } from './generators/template'
@@ -38,10 +38,6 @@ export class CodegenContext {
   identifiers: Record<string, string[]> = Object.create(null)
 
   block: BlockIRNode
-  genEffects: Array<
-    (effects: IREffect[], context: CodegenContext) => CodeFragment[]
-  > = []
-
   withId<T>(fn: () => T, map: Record<string, string | null>): T {
     const { identifiers } = this
     const ids = Object.keys(map)

+ 1 - 5
packages/compiler-vapor/src/generators/block.ts

@@ -53,11 +53,7 @@ export function genBlockContent(
   }
 
   push(...genOperations(operation, context))
-  push(
-    ...(context.genEffects.length
-      ? context.genEffects[context.genEffects.length - 1]
-      : genEffects)(effect, context),
-  )
+  push(...genEffects(effect, context))
 
   push(NEWLINE, `return `)
 

+ 61 - 65
packages/compiler-vapor/src/generators/for.ts

@@ -1,15 +1,14 @@
-import { NewlineType } from '@vue/compiler-dom'
+import { walkIdentifiers } from '@vue/compiler-dom'
 import { genBlock } from './block'
 import { genExpression } from './expression'
 import type { CodegenContext } from '../generate'
-import type { ForIRNode, IREffect } from '../ir'
-import { genOperations } from './operation'
+import type { ForIRNode } from '../ir'
 import {
   type CodeFragment,
+  DELIMITERS_ARRAY,
   INDENT_END,
   INDENT_START,
   NEWLINE,
-  buildCodeFragment,
   genCall,
   genMulti,
 } from './utils'
@@ -18,40 +17,52 @@ export function genFor(
   oper: ForIRNode,
   context: CodegenContext,
 ): CodeFragment[] {
-  const { vaporHelper, genEffects } = context
-  const { source, value, key, index, render, keyProp, once } = oper
+  const { vaporHelper } = context
+  const { source, value, key, index, render, keyProp, once, id } = oper
 
-  const rawValue = value && value.content
+  let isDestructureAssignment = false
+  let rawValue: string | null = null
   const rawKey = key && key.content
   const rawIndex = index && index.content
 
   const sourceExpr = ['() => (', ...genExpression(source, context), ')']
-  let updateFn = '_updateEffect'
 
-  genEffects.push(genEffectInFor)
-
-  const idMap: Record<string, string> = {}
-  if (rawValue) idMap[rawValue] = `_block.s[0]`
-  if (rawKey) idMap[rawKey] = `_block.s[1]`
-  if (rawIndex) idMap[rawIndex] = `_block.s[2]`
+  const idsOfValue = new Set<string>()
+  if (value) {
+    rawValue = value && value.content
+    if ((isDestructureAssignment = !!value.ast)) {
+      walkIdentifiers(
+        value.ast,
+        (id, _, __, ___, isLocal) => {
+          if (isLocal) idsOfValue.add(id.name)
+        },
+        true,
+      )
+    } else {
+      idsOfValue.add(rawValue)
+    }
+  }
 
-  const blockReturns = (returns: CodeFragment[]): CodeFragment[] => [
-    '[',
-    ...returns,
-    `, ${updateFn}]`,
-  ]
+  const propsName = `_ctx${id}`
+  const idMap: Record<string, string | null> = {}
+  Array.from(idsOfValue).forEach(
+    (id, idIndex) => (idMap[id] = `${propsName}[${idIndex}]`),
+  )
+  if (rawKey) idMap[rawKey] = `${propsName}[${idsOfValue.size}]`
+  if (rawIndex) idMap[rawIndex] = `${propsName}[${idsOfValue.size + 1}]`
 
   const blockFn = context.withId(
-    () => genBlock(render, context, ['_block'], false, blockReturns),
+    () => genBlock(render, context, [propsName]),
     idMap,
   )
 
   let getKeyFn: CodeFragment[] | false = false
   if (keyProp) {
     const idMap: Record<string, null> = {}
-    if (rawValue) idMap[rawValue] = null
     if (rawKey) idMap[rawKey] = null
     if (rawIndex) idMap[rawIndex] = null
+    idsOfValue.forEach(id => (idMap[id] = null))
+
     const expr = context.withId(() => genExpression(keyProp, context), idMap)
     getKeyFn = [
       ...genMulti(
@@ -66,7 +77,33 @@ export function genFor(
     ]
   }
 
-  genEffects.pop()
+  let destructureAssignmentFn: CodeFragment[] | false = false
+  if (isDestructureAssignment) {
+    const idMap: Record<string, null> = {}
+    idsOfValue.forEach(id => (idMap[id] = null))
+    if (rawKey) idMap[rawKey] = null
+    if (rawIndex) idMap[rawIndex] = null
+    destructureAssignmentFn = [
+      '_state => {',
+      INDENT_START,
+      NEWLINE,
+      'const ',
+      ...genMulti(
+        DELIMITERS_ARRAY,
+        rawValue ? rawValue : rawKey || rawIndex ? '_' : undefined,
+        rawKey ? rawKey : rawIndex ? '__' : undefined,
+        rawIndex,
+      ),
+      ' = _state',
+      NEWLINE,
+      'return ',
+      ...genMulti(DELIMITERS_ARRAY, ...idsOfValue, rawKey, rawIndex),
+      INDENT_END,
+      NEWLINE,
+      '}',
+    ]
+  }
+
   return [
     NEWLINE,
     `const n${oper.id} = `,
@@ -77,49 +114,8 @@ export function genFor(
       getKeyFn,
       false, // todo: getMemo
       false, // todo: hydrationNode
-      once && 'true',
+      (once && 'true') || (destructureAssignmentFn && 'false'),
+      destructureAssignmentFn,
     ),
   ]
-
-  function genEffectInFor(effects: IREffect[]): CodeFragment[] {
-    if (!effects.length) {
-      updateFn = '() => {}'
-      return []
-    }
-
-    const [frag, push] = buildCodeFragment(INDENT_START)
-    // const [value, key] = _block.s
-    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)
-
-    push(INDENT_END)
-
-    return [
-      NEWLINE,
-      `const ${updateFn} = () => {`,
-      ...frag,
-      NEWLINE,
-      '}',
-      NEWLINE,
-      `${vaporHelper('renderEffect')}(${updateFn})`,
-    ]
-  }
 }

+ 15 - 18
packages/runtime-vapor/__tests__/dom/templateRef.spec.ts

@@ -355,15 +355,14 @@ describe('api: template ref', () => {
             const n1 = t0()
             const n2 = createFor(
               () => list,
-              _block => {
+              state => {
                 const n1 = t1()
                 setRef(n1 as Element, listRefs, undefined, true)
-                const updateEffect = () => {
-                  const [item] = _block.s
+                renderEffect(() => {
+                  const [item] = state
                   setText(n1, item)
-                }
-                renderEffect(updateEffect)
-                return [n1, updateEffect]
+                })
+                return n1
               },
             )
             insert(n2, n1 as ParentNode)
@@ -414,15 +413,14 @@ describe('api: template ref', () => {
             const n1 = t0()
             const n2 = createFor(
               () => list,
-              _block => {
+              state => {
                 const n1 = t1()
                 setRef(n1 as Element, 'listRefs', undefined, true)
-                const updateEffect = () => {
-                  const [item] = _block.s
+                renderEffect(() => {
+                  const [item] = state
                   setText(n1, item)
-                }
-                renderEffect(updateEffect)
-                return [n1, updateEffect]
+                })
+                return n1
               },
             )
             insert(n2, n1 as ParentNode)
@@ -471,15 +469,14 @@ describe('api: template ref', () => {
         const n2 = n1!.nextSibling!
         const n3 = createFor(
           () => list.value,
-          _block => {
+          state => {
             const n4 = t1()
             setRef(n4 as Element, 'listRefs', undefined, true)
-            const updateEffect = () => {
-              const [item] = _block.s
+            renderEffect(() => {
+              const [item] = state
               setText(n4, item)
-            }
-            renderEffect(updateEffect)
-            return [n4, updateEffect]
+            })
+            return n4
           },
         )
         insert(n3, n2 as unknown as ParentNode)

+ 200 - 25
packages/runtime-vapor/__tests__/for.spec.ts

@@ -1,4 +1,3 @@
-import { NOOP } from '@vue/shared'
 import {
   type Directive,
   children,
@@ -6,6 +5,7 @@ import {
   nextTick,
   ref,
   renderEffect,
+  shallowRef,
   template,
   withDirectives,
 } from '../src'
@@ -24,17 +24,16 @@ describe('createFor', () => {
     const { host } = define(() => {
       const n1 = createFor(
         () => list.value,
-        block => {
+        state => {
           const span = document.createElement('li')
-          const update = () => {
-            const [item, key, index] = block.s
+          renderEffect(() => {
+            const [item, key, index] = state
             span.innerHTML = `${key}. ${item.name}`
 
             // index should be undefined if source is not an object
             expect(index).toBe(undefined)
-          }
-          renderEffect(update)
-          return [span, update]
+          })
+          return span
         },
         item => item.name,
       )
@@ -91,17 +90,16 @@ describe('createFor', () => {
     const { host } = define(() => {
       const n1 = createFor(
         () => count.value,
-        block => {
+        state => {
           const span = document.createElement('li')
-          const update = () => {
-            const [item, key, index] = block.s
+          renderEffect(() => {
+            const [item, key, index] = state
             span.innerHTML = `${key}. ${item}`
 
             // index should be undefined if source is not an object
             expect(index).toBe(undefined)
-          }
-          renderEffect(update)
-          return [span, update]
+          })
+          return span
         },
         item => item.name,
       )
@@ -137,15 +135,14 @@ describe('createFor', () => {
     const { host } = define(() => {
       const n1 = createFor(
         () => data.value,
-        block => {
+        state => {
           const span = document.createElement('li')
-          const update = () => {
-            const [item, key, index] = block.s
+          renderEffect(() => {
+            const [item, key, index] = state
             span.innerHTML = `${key}${index}. ${item}`
             expect(index).not.toBe(undefined)
-          }
-          renderEffect(update)
-          return [span, update]
+          })
+          return span
         },
         item => {
           return item
@@ -215,11 +212,14 @@ describe('createFor', () => {
 
     const t0 = template('<p></p>')
     const { instance } = define(() => {
-      const n1 = createFor(spySrcFn, block => {
+      const n1 = createFor(spySrcFn, ctx0 => {
         const n2 = t0()
         const n3 = children(n2, 0)
-        withDirectives(n3, [[vDirective, () => block.s[0]]])
-        return [n2, NOOP]
+        withDirectives(n3, [[vDirective, () => ctx0[0]]])
+        renderEffect(() => {
+          calls.push(`${ctx0[0]} effecting`)
+        })
+        return n2
       })
       renderEffect(() => update.value)
       return [n1]
@@ -227,7 +227,12 @@ describe('createFor', () => {
 
     await nextTick()
     // `${item index} ${hook name}`
-    expect(calls).toEqual(['0 created', '0 beforeMount', '0 mounted'])
+    expect(calls).toEqual([
+      '0 created',
+      '0 effecting',
+      '0 beforeMount',
+      '0 mounted',
+    ])
     calls.length = 0
     expect(spySrcFn).toHaveBeenCalledTimes(1)
 
@@ -236,6 +241,7 @@ describe('createFor', () => {
     expect(calls).toEqual([
       '0 beforeUpdate',
       '1 created',
+      '1 effecting',
       '1 beforeMount',
       '0 updated',
       '1 mounted',
@@ -248,6 +254,8 @@ describe('createFor', () => {
     expect(calls).toEqual([
       '1 beforeUpdate',
       '0 beforeUpdate',
+      '1 effecting',
+      '0 effecting',
       '1 updated',
       '0 updated',
     ])
@@ -268,6 +276,23 @@ describe('createFor', () => {
     calls.length = 0
     expect(spySrcFn).toHaveBeenCalledTimes(4)
 
+    // change item
+    list.value[1] = 2
+    await nextTick()
+    expect(calls).toEqual([
+      '0 beforeUpdate',
+      '2 beforeUpdate',
+      '2 effecting',
+      '0 updated',
+      '2 updated',
+    ])
+    expect(spySrcFn).toHaveBeenCalledTimes(5)
+    list.value[1] = 1
+    await nextTick()
+    calls.length = 0
+    expect(spySrcFn).toHaveBeenCalledTimes(6)
+
+    // remove the last item
     list.value.pop()
     await nextTick()
     expect(calls).toEqual([
@@ -277,10 +302,160 @@ describe('createFor', () => {
       '1 unmounted',
     ])
     calls.length = 0
-    expect(spySrcFn).toHaveBeenCalledTimes(5)
+    expect(spySrcFn).toHaveBeenCalledTimes(7)
 
     unmountComponent(instance)
     expect(calls).toEqual(['0 beforeUnmount', '0 unmounted'])
-    expect(spySrcFn).toHaveBeenCalledTimes(5)
+    expect(spySrcFn).toHaveBeenCalledTimes(7)
+  })
+
+  test('de-structured value', async () => {
+    const list = ref([{ name: '1' }, { name: '2' }, { name: '3' }])
+    function reverse() {
+      list.value = list.value.reverse()
+    }
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        state => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            const [name, key, index] = state
+            span.innerHTML = `${key}. ${name}`
+
+            // index should be undefined if source is not an object
+            expect(index).toBe(undefined)
+          })
+          return span
+        },
+        item => item.name,
+        undefined,
+        undefined,
+        false,
+        state => {
+          const [{ name }, key, index] = state
+          return [name, key, index]
+        },
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><!--for-->',
+    )
+
+    // add
+    list.value.push({ name: '4' })
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // move
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 4</li><li>1. 3</li><li>2. 2</li><li>3. 1</li><!--for-->',
+    )
+
+    reverse()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // change
+    list.value[0].name = 'a'
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // remove
+    list.value.splice(1, 1)
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 3</li><li>2. 4</li><!--for-->',
+    )
+
+    // clear
+    list.value = []
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
+  })
+
+  test('shallowRef source', async () => {
+    const list = shallowRef([{ name: '1' }, { name: '2' }, { name: '3' }])
+    const setList = (update = list.value.slice()) => (list.value = update)
+    function reverse() {
+      list.value = list.value.reverse()
+    }
+
+    const { host } = define(() => {
+      const n1 = createFor(
+        () => list.value,
+        state => {
+          const span = document.createElement('li')
+          renderEffect(() => {
+            const [item, key, index] = state
+            span.innerHTML = `${key}. ${item.name}`
+
+            // index should be undefined if source is not an object
+            expect(index).toBe(undefined)
+          })
+          return span
+        },
+      )
+      return n1
+    }).render()
+
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><!--for-->',
+    )
+
+    // add
+    list.value.push({ name: '4' })
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // move
+    reverse()
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 4</li><li>1. 3</li><li>2. 2</li><li>3. 1</li><!--for-->',
+    )
+
+    reverse()
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // change
+    list.value[0].name = 'a'
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
+    )
+
+    // remove
+    list.value.splice(1, 1)
+    setList()
+    await nextTick()
+    expect(host.innerHTML).toBe(
+      '<li>0. a</li><li>1. 3</li><li>2. 4</li><!--for-->',
+    )
+
+    // clear
+    setList([])
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--for-->')
   })
 })

+ 64 - 33
packages/runtime-vapor/src/apiCreateFor.ts

@@ -1,4 +1,12 @@
-import { getCurrentScope, isReactive, traverse } from '@vue/reactivity'
+import {
+  type ShallowRef,
+  getCurrentScope,
+  isReactive,
+  proxyRefs,
+  shallowRef,
+  traverse,
+  triggerRef,
+} from '@vue/reactivity'
 import { isArray, isObject, isString } from '@vue/shared'
 import {
   createComment,
@@ -18,12 +26,15 @@ import {
   invokeWithUpdate,
 } from './directivesChildFragment'
 import type { DynamicSlot } from './componentSlots'
+import { destructuring } from './destructuring'
 
 interface ForBlock extends Fragment {
   scope: BlockEffectScope
-  /** state, use short key since it's used a lot in generated code */
-  s: [item: any, key: any, index?: number]
-  update: () => void
+  state: [
+    item: ShallowRef<any>,
+    key: ShallowRef<any>,
+    index: ShallowRef<number | undefined>,
+  ]
   key: any
   memo: any[] | undefined
 }
@@ -33,11 +44,12 @@ type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
 /*! #__NO_SIDE_EFFECTS__ */
 export const createFor = (
   src: () => Source,
-  renderItem: (block: ForBlock) => [Block, () => void],
+  renderItem: (block: any) => Block,
   getKey?: (item: any, key: any, index?: number) => any,
   getMemo?: (item: any, key: any, index?: number) => any[],
   hydrationNode?: Node,
   once?: boolean,
+  assignment?: (state: any[]) => any[],
 ): Fragment => {
   let isMounted = false
   let oldBlocks: ForBlock[] = []
@@ -258,21 +270,28 @@ export const createFor = (
     const scope = new BlockEffectScope(instance, parentScope)
 
     const [item, key, index] = getItem(source, idx)
+    const state = [
+      shallowRef(item),
+      shallowRef(key),
+      shallowRef(index),
+    ] as ForBlock['state']
     const block: ForBlock = (newBlocks[idx] = {
       nodes: null!, // set later
-      update: null!, // set later
       scope,
-      s: [item, key, index],
+      state,
       key: getKey && getKey(item, key, index),
       memo: getMemo && getMemo(item, key, index),
       [fragmentKey]: true,
     })
-    const res = scope.run(() => renderItem(block))!
-    block.nodes = res[0]
-    block.update = res[1]
+    const proxyState = proxyRefs(state)
+    const itemCtx = assignment
+      ? destructuring(scope, proxyState, assignment)
+      : proxyState
+    block.nodes = scope.run(() => renderItem(itemCtx))!
 
     invokeWithMount(scope, () => {
-      if (getMemo) block.update()
+      // TODO v-memo
+      // if (getMemo) block.update()
       if (parent) insert(block.nodes, parent, anchor)
     })
 
@@ -297,10 +316,11 @@ export const createFor = (
   function updateWithMemo(
     block: ForBlock,
     newItem: any,
-    newKey = block.s[1],
-    newIndex = block.s[2],
+    newKey = block.state[1].value,
+    newIndex = block.state[2].value,
   ) {
-    let needsUpdate = newKey !== block.s[1] || newIndex !== block.s[2]
+    const [, key, index] = block.state
+    let needsUpdate = newKey !== key.value || newIndex !== index.value
     if (!needsUpdate) {
       const oldMemo = block.memo!
       const newMemo = (block.memo = getMemo!(newItem, newKey, newIndex))
@@ -311,32 +331,26 @@ export const createFor = (
       }
     }
 
-    block.s = [newItem, newKey, newIndex]
-    invokeWithUpdate(block.scope, () => {
-      if (needsUpdate) {
-        block.update()
-      }
-    })
+    if (needsUpdate) setState(block, newItem, newKey, newIndex)
+    invokeWithUpdate(block.scope)
   }
 
   function updateWithoutMemo(
     block: ForBlock,
     newItem: any,
-    newKey = block.s[1],
-    newIndex = block.s[2],
+    newKey = block.state[1].value,
+    newIndex = block.state[2].value,
   ) {
+    const [item, key, index] = block.state
     let needsUpdate =
-      newItem !== block.s[0] ||
-      newKey !== block.s[1] ||
-      newIndex !== block.s[2] ||
-      !isReactive(newItem)
-
-    block.s = [newItem, newKey, newIndex]
-    invokeWithUpdate(block.scope, () => {
-      if (needsUpdate) {
-        block.update()
-      }
-    })
+      newItem !== item.value ||
+      newKey !== key.value ||
+      newIndex !== index.value ||
+      // shallowRef list
+      (!isReactive(newItem) && isObject(newItem))
+
+    if (needsUpdate) setState(block, newItem, newKey, newIndex)
+    invokeWithUpdate(block.scope)
   }
 
   function unmount({ nodes, scope }: ForBlock) {
@@ -346,6 +360,23 @@ export const createFor = (
   }
 }
 
+function setState(
+  block: ForBlock,
+  newItem: any,
+  newKey: any,
+  newIndex: number | undefined,
+) {
+  const [item, key, index] = block.state
+  const oldItem = item.value
+  item.value = newItem
+  key.value = newKey
+  index.value = newIndex
+
+  if (oldItem === newItem && !isReactive(oldItem)) {
+    triggerRef(item)
+  }
+}
+
 export function createForSlots(
   source: any[] | Record<any, any> | number | Set<any> | Map<any, any>,
   getSlot: (item: any, key: any, index?: number) => DynamicSlot,

+ 20 - 0
packages/runtime-vapor/src/destructuring.ts

@@ -0,0 +1,20 @@
+import { type EffectScope, shallowReactive } from '@vue/reactivity'
+import { renderEffect } from './renderEffect'
+
+export function destructuring(
+  scope: EffectScope,
+  state: any,
+  fn: (state: any) => any[],
+) {
+  const list = shallowReactive<any[]>([])
+  scope.run(() => {
+    renderEffect(() => {
+      const res = fn(state)
+      const len = res.length
+      for (let i = 0; i < len; i++) {
+        list[i] = res[i]
+      }
+    })
+  })
+  return list
+}

+ 21 - 23
packages/vue-vapor/examples/composition/todomvc.html

@@ -149,10 +149,10 @@ const _sfc_main = defineComponent({
   },
 })
 
-const { children: _children, vModelText: _vModelText, withDirectives: _withDirectives, vShow: _vShow, next: _next, delegate: _delegate, on: _on, setDynamicProp: _setDynamicProp, setText: _setText, setClass: _setClass, renderEffect: _renderEffect, createFor: _createFor, insert: _insert, delegateEvents: _delegateEvents, template: _template } = VueVapor
+const { children: _children, vModelText: _vModelText, withDirectives: _withDirectives, vShow: _vShow, next: _next, delegate: _delegate, on: _on, renderEffect: _renderEffect, setDynamicProp: _setDynamicProp, setText: _setText, setClass: _setClass, createFor: _createFor, insert: _insert, delegateEvents: _delegateEvents, template: _template } = VueVapor
 const t0 = _template("<li><div class=\"view\"><input class=\"toggle\" type=\"checkbox\"><label></label><button class=\"destroy\"></button></div><input class=\"edit\" type=\"text\"></li>")
 const t1 = _template("<section class=\"todoapp\"><header class=\"header\"><h1>todos</h1><input class=\"new-todo\" autofocus autocomplete=\"off\" placeholder=\"What needs to be done?\"></header><section class=\"main\"><input id=\"toggle-all\" class=\"toggle-all\" type=\"checkbox\"><label for=\"toggle-all\">Mark all as complete</label><ul class=\"todo-list\"></ul></section><footer class=\"footer\"><span class=\"todo-count\"><strong></strong><span></span></span><ul class=\"filters\"><li><a href=\"#/all\">All</a></li><li><a href=\"#/active\">Active</a></li><li><a href=\"#/completed\">Completed</a></li></ul><button class=\"clear-completed\"> Clear completed </button></footer></section>")
-_delegateEvents("keyup", "dblclick", "click")
+_delegateEvents("keyup", "dblclick", "click", "input")
 
 function _sfc_render(_ctx) {
   const n18 = t1()
@@ -176,36 +176,34 @@ function _sfc_render(_ctx) {
     keys: ["enter"]
   })
   _on(n1, "change", () => $event => (_ctx.state.allDone = $event.target.checked))
-  const n2 = _createFor(() => (_ctx.state.filteredTodos), (_block) => {
+  const n2 = _createFor(() => (_ctx.state.filteredTodos), (_ctx2) => {
     const n8 = t0()
     const n4 = _children(n8, 0, 0)
     const n5 = n4.nextSibling
     const n6 = n5.nextSibling
     const n7 = _children(n8, 1)
-    _on(n4, "change", () => $event => (_block.s[0].completed = $event.target.checked))
-    _delegate(n5, "dblclick", () => $event => (_ctx.editTodo(_block.s[0])))
-    _delegate(n6, "click", () => $event => (_ctx.removeTodo(_block.s[0])))
-    _on(n7, "input", () => $event => (_block.s[0].title = $event.target.value))
-    _on(n7, "blur", () => $event => (_ctx.doneEdit(_block.s[0])))
-    _delegate(n7, "keyup", () => $event => (_ctx.doneEdit(_block.s[0])), {
+    _on(n4, "change", () => $event => (_ctx2[0].completed = $event.target.checked))
+    _delegate(n5, "dblclick", () => $event => (_ctx.editTodo(_ctx2[0])))
+    _delegate(n6, "click", () => $event => (_ctx.removeTodo(_ctx2[0])))
+    _delegate(n7, "input", () => $event => (_ctx2[0].title = $event.target.value))
+    _on(n7, "blur", () => $event => (_ctx.doneEdit(_ctx2[0])))
+    _delegate(n7, "keyup", () => $event => (_ctx.doneEdit(_ctx2[0])), {
       keys: ["enter"]
     })
-    _delegate(n7, "keyup", () => $event => (_ctx.cancelEdit(_block.s[0])), {
+    _delegate(n7, "keyup", () => $event => (_ctx.cancelEdit(_ctx2[0])), {
       keys: ["escape"]
     })
-    const _updateEffect = () => {
-      const [todo] = _block.s
-      _setDynamicProp(n4, "checked", todo.completed)
-      _setText(n5, todo.title)
-      _setDynamicProp(n7, "value", todo.title)
-      _setDynamicProp(n7, "id", `todo-${todo.id}-input`)
-      _setClass(n8, ["todo", {
-        completed: todo.completed,
-        editing: todo === _ctx.state.editedTodo,
-      }])
-    }
-    _renderEffect(_updateEffect)
-    return [n8, _updateEffect]
+    _renderEffect(() => _setDynamicProp(n4, "checked", _ctx2[0].completed))
+    _renderEffect(() => {
+      _setText(n5, _ctx2[0].title)
+      _setDynamicProp(n7, "value", _ctx2[0].title)
+    })
+    _renderEffect(() => _setDynamicProp(n7, "id", `todo-${_ctx2[0].id}-input`))
+    _renderEffect(() => _setClass(n8, ["todo", {
+            completed: _ctx2[0].completed,
+            editing: _ctx2[0] === _ctx.state.editedTodo,
+          }]))
+    return n8
   }, (todo) => (todo.id))
   _insert(n2, n9)
   _delegate(n16, "click", () => _ctx.removeCompleted)

+ 5 - 7
playground/src/v-for.js

@@ -26,18 +26,16 @@ export default defineComponent({
     return (() => {
       const li = createFor(
         () => list.value,
-        block => {
+        ctx0 => {
           const node = document.createTextNode('')
           const container = document.createElement('li')
           insert(node, container)
 
-          const update = () => {
-            const [item, index] = block.s
+          renderEffect(() => {
+            const [item, index] = ctx0
             node.textContent = `${index}. ${item}`
-          }
-
-          renderEffect(update)
-          return [container, update]
+          })
+          return container
         },
         (item, index) => index,
       )