diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
index bf50a8e82..a60019925 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
+++ b/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("
")
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
}"
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
index 8732e4eda..7739fcb0c 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
+++ b/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("")
+
+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("")
_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("")
+
+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("")
+const t1 = _template("")
+
+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("")
+
+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("")
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("item
")
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
}"
diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
index f24f4c875..f4e42a895 100644
--- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
+++ b/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("")
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
}"
diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
index 0f4785734..f457ac745 100644
--- a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
+++ b/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(
+ `{{ j+i }}
`,
+ )
+ 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(['', ''])
+ 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(
+ `{{ id + other + index }}
`,
+ )
+ 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(
+ `{{ id + other + index }}
`,
+ )
+ 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(
+ `
+ {{ foo + bar + baz + qux + quux }}
+
`,
+ )
+ 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,
+ })
+ })
})
diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts
index 07c56f8f8..f6f4b463f 100644
--- a/packages/compiler-vapor/src/generate.ts
+++ b/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 = Object.create(null)
block: BlockIRNode
- genEffects: Array<
- (effects: IREffect[], context: CodegenContext) => CodeFragment[]
- > = []
-
withId(fn: () => T, map: Record): T {
const { identifiers } = this
const ids = Object.keys(map)
diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts
index 3ce28cc1d..5200e8316 100644
--- a/packages/compiler-vapor/src/generators/block.ts
+++ b/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 `)
diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts
index 738ef12e6..9c3b962f9 100644
--- a/packages/compiler-vapor/src/generators/for.ts
+++ b/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 = {}
- 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()
+ 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 = {}
+ 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 = {}
- 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 = {}
+ 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 = {}
- 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})`,
- ]
- }
}
diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
index 41997b31a..b8d349c8f 100644
--- a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
+++ b/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)
diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts
index fba012c62..2d028d069 100644
--- a/packages/runtime-vapor/__tests__/for.spec.ts
+++ b/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('')
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(
+ '0. 11. 22. 3',
+ )
+
+ // add
+ list.value.push({ name: '4' })
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. 11. 22. 33. 4',
+ )
+
+ // move
+ reverse()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. 41. 32. 23. 1',
+ )
+
+ reverse()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. 11. 22. 33. 4',
+ )
+
+ // change
+ list.value[0].name = 'a'
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. a1. 22. 33. 4',
+ )
+
+ // remove
+ list.value.splice(1, 1)
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. a1. 32. 4',
+ )
+
+ // clear
+ list.value = []
+ await nextTick()
+ expect(host.innerHTML).toBe('')
+ })
+
+ 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(
+ '0. 11. 22. 3',
+ )
+
+ // add
+ list.value.push({ name: '4' })
+ setList()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. 11. 22. 33. 4',
+ )
+
+ // move
+ reverse()
+ setList()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. 41. 32. 23. 1',
+ )
+
+ reverse()
+ setList()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. 11. 22. 33. 4',
+ )
+
+ // change
+ list.value[0].name = 'a'
+ setList()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. a1. 22. 33. 4',
+ )
+
+ // remove
+ list.value.splice(1, 1)
+ setList()
+ await nextTick()
+ expect(host.innerHTML).toBe(
+ '0. a1. 32. 4',
+ )
+
+ // clear
+ setList([])
+ await nextTick()
+ expect(host.innerHTML).toBe('')
})
})
diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts
index 3bad7f36b..446141700 100644
--- a/packages/runtime-vapor/src/apiCreateFor.ts
+++ b/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,
+ key: ShallowRef,
+ index: ShallowRef,
+ ]
key: any
memo: any[] | undefined
}
@@ -33,11 +44,12 @@ type Source = any[] | Record | number | Set | Map
/*! #__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 | number | Set | Map,
getSlot: (item: any, key: any, index?: number) => DynamicSlot,
diff --git a/packages/runtime-vapor/src/destructuring.ts b/packages/runtime-vapor/src/destructuring.ts
new file mode 100644
index 000000000..255130dac
--- /dev/null
+++ b/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([])
+ scope.run(() => {
+ renderEffect(() => {
+ const res = fn(state)
+ const len = res.length
+ for (let i = 0; i < len; i++) {
+ list[i] = res[i]
+ }
+ })
+ })
+ return list
+}
diff --git a/packages/vue-vapor/examples/composition/todomvc.html b/packages/vue-vapor/examples/composition/todomvc.html
index 47faa6412..0e3aef787 100644
--- a/packages/vue-vapor/examples/composition/todomvc.html
+++ b/packages/vue-vapor/examples/composition/todomvc.html
@@ -149,10 +149,10 @@
},
})
-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("")
const t1 = _template("")
-_delegateEvents("keyup", "dblclick", "click")
+_delegateEvents("keyup", "dblclick", "click", "input")
function _sfc_render(_ctx) {
const n18 = t1()
@@ -176,36 +176,34 @@
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)
diff --git a/playground/src/v-for.js b/playground/src/v-for.js
index 4662d76ee..3a1c2cc3d 100644
--- a/playground/src/v-for.js
+++ b/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,
)