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. 1
  • 1. 2
  • 2. 3
  • ', + ) + + // add + list.value.push({ name: '4' }) + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. 1
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + ) + + // move + reverse() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. 4
  • 1. 3
  • 2. 2
  • 3. 1
  • ', + ) + + reverse() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. 1
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + ) + + // change + list.value[0].name = 'a' + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. a
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + ) + + // remove + list.value.splice(1, 1) + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. a
  • 1. 3
  • 2. 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. 1
  • 1. 2
  • 2. 3
  • ', + ) + + // add + list.value.push({ name: '4' }) + setList() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. 1
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + ) + + // move + reverse() + setList() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. 4
  • 1. 3
  • 2. 2
  • 3. 1
  • ', + ) + + reverse() + setList() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. 1
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + ) + + // change + list.value[0].name = 'a' + setList() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. a
  • 1. 2
  • 2. 3
  • 3. 4
  • ', + ) + + // remove + list.value.splice(1, 1) + setList() + await nextTick() + expect(host.innerHTML).toBe( + '
  • 0. a
  • 1. 3
  • 2. 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("

    todos

      ") -_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, )