From cad7f0e583acc7f3f4774bba0216d43210a5e2aa Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Jan 2025 13:14:16 +0800 Subject: [PATCH] wip(vapor): improve v-for codegen + minor optimization --- .../transformTemplateRef.spec.ts.snap | 2 +- .../__snapshots__/vFor.spec.ts.snap | 40 +++++----- .../__snapshots__/vOnce.spec.ts.snap | 2 +- .../__tests__/transforms/vFor.spec.ts | 18 ++--- packages/compiler-vapor/src/generators/for.ts | 27 +++++-- .../__tests__/apiCreateSelector.spec.ts | 4 +- .../__tests__/dom/templateRef.spec.ts | 9 +-- packages/runtime-vapor/__tests__/for.spec.ts | 54 ++++++-------- packages/runtime-vapor/src/apiCreateFor.ts | 74 +++++++++++-------- 9 files changed, 119 insertions(+), 111 deletions(-) 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 da863b2407f..302e240ca50 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap @@ -19,7 +19,7 @@ const t0 = _template("
", true) export function render(_ctx) { const _setTemplateRef = _createTemplateRefSetter() - const n0 = _createFor(() => ([1,2,3]), (_ctx0) => { + const n0 = _createFor(() => ([1,2,3]), (_for_item0) => { const n2 = t0() _setTemplateRef(n2, "foo", void 0, true) return n2 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 694b804cd4b..21b4b1d4a39 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -5,9 +5,9 @@ exports[`compiler: v-for > array de-structured value (with rest) 1`] = ` const t0 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.list), (_ctx0) => { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { const n2 = t0() - _renderEffect(() => _setText(n2, _ctx0[0].value[0] + _ctx0[0].value.slice(1) + _ctx0[1].value)) + _renderEffect(() => _setText(n2, _for_item0.value[0] + _for_item0.value.slice(1) + _for_key0.value)) return n2 }, ([id, ...other], index) => (id)) return n0 @@ -19,9 +19,9 @@ exports[`compiler: v-for > array de-structured value 1`] = ` const t0 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.list), (_ctx0) => { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { const n2 = t0() - _renderEffect(() => _setText(n2, _ctx0[0].value[0] + _ctx0[0].value[1] + _ctx0[1].value)) + _renderEffect(() => _setText(n2, _for_item0.value[0] + _for_item0.value[1] + _for_key0.value)) return n2 }, ([id, other], index) => (id)) return n0 @@ -34,10 +34,10 @@ const t0 = _template("
", true) _delegateEvents("click") export function render(_ctx) { - const n0 = _createFor(() => (_ctx.items), (_ctx0) => { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { const n2 = t0() - _delegate(n2, "click", () => $event => (_ctx.remove(_ctx0[0].value))) - _renderEffect(() => _setText(n2, _ctx0[0].value)) + _delegate(n2, "click", () => $event => (_ctx.remove(_for_item0.value))) + _renderEffect(() => _setText(n2, _for_item0.value)) return n2 }, (item) => (item.id)) return n0 @@ -49,11 +49,11 @@ exports[`compiler: v-for > multi effect 1`] = ` const t0 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.items), (_ctx0) => { + const n0 = _createFor(() => (_ctx.items), (_for_item0, _for_key0) => { const n2 = t0() _renderEffect(() => { - _setProp(n2, "item", _ctx0[0].value) - _setProp(n2, "index", _ctx0[1].value) + _setProp(n2, "item", _for_item0.value) + _setProp(n2, "index", _for_key0.value) }) return n2 }) @@ -67,11 +67,11 @@ const t0 = _template("") const t1 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.list), (_ctx0) => { + const n0 = _createFor(() => (_ctx.list), (_for_item0) => { const n5 = t1() - const n2 = _createFor(() => (_ctx0[0].value), (_ctx1) => { + const n2 = _createFor(() => (_for_item0.value), (_for_item1) => { const n4 = t0() - _renderEffect(() => _setText(n4, _ctx1[0].value+_ctx0[0].value)) + _renderEffect(() => _setText(n4, _for_item1.value+_for_item0.value)) return n4 }) _insert(n2, n5) @@ -86,9 +86,9 @@ exports[`compiler: v-for > object de-structured value (with rest) 1`] = ` const t0 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.list), (_ctx0) => { + const n0 = _createFor(() => (_ctx.list), (_for_item0, _for_key0) => { const n2 = t0() - _renderEffect(() => _setText(n2, _ctx0[0].value.id + _getRestElement(_ctx0[0].value, ["id"]) + _ctx0[1].value)) + _renderEffect(() => _setText(n2, _for_item0.value.id + _getRestElement(_for_item0.value, ["id"]) + _for_key0.value)) return n2 }, ({ id, ...other }, index) => (id)) return n0 @@ -100,9 +100,9 @@ exports[`compiler: v-for > object de-structured value 1`] = ` const t0 = _template("", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.items), (_ctx0) => { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { const n2 = t0() - _renderEffect(() => _setText(n2, _ctx0[0].value.id, _ctx0[0].value.value)) + _renderEffect(() => _setText(n2, _for_item0.value.id, _for_item0.value.value)) return n2 }, ({ id, value }) => (id)) return n0 @@ -114,9 +114,9 @@ exports[`compiler: v-for > v-for aliases w/ complex expressions 1`] = ` const t0 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.list), (_ctx0) => { + const n0 = _createFor(() => (_ctx.list), (_for_item0) => { const n2 = t0() - _renderEffect(() => _setText(n2, _getDefaultValue(_ctx._ctx0[0].value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_ctx._ctx0[0].value.baz[0], _ctx.quux) + _ctx.quux)) + _renderEffect(() => _setText(n2, _getDefaultValue(_for_item0.value.foo, _ctx.bar) + _ctx.bar + _ctx.baz + _getDefaultValue(_for_item0.value.baz[0], _ctx.quux) + _ctx.quux)) return n2 }) return n0 @@ -128,7 +128,7 @@ exports[`compiler: v-for > w/o value 1`] = ` const t0 = _template("
item
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.items), (_ctx0) => { + const n0 = _createFor(() => (_ctx.items), (_for_item0) => { const n2 = t0() return n2 }) 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 15fcd2182a8..1f5b6c63476 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap @@ -65,7 +65,7 @@ exports[`compiler: v-once > with v-for 1`] = ` const t0 = _template("
", true) export function render(_ctx) { - const n0 = _createFor(() => (_ctx.list), (_ctx0) => { + const n0 = _createFor(() => (_ctx.list), (_for_item0) => { const n2 = t0() return n2 }, null, null, true) diff --git a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts index 0e2a7bd826e..22ded75aeb9 100644 --- a/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vFor.spec.ts @@ -84,9 +84,11 @@ describe('compiler: v-for', () => { `
{{ j+i }}
`, ) expect(code).matchSnapshot() - expect(code).contains(`_createFor(() => (_ctx.list), (_ctx0) => {`) - expect(code).contains(`_createFor(() => (_ctx0[0].value), (_ctx1) => {`) - expect(code).contains(`_ctx1[0].value+_ctx0[0].value`) + expect(code).contains(`_createFor(() => (_ctx.list), (_for_item0) => {`) + expect(code).contains( + `_createFor(() => (_for_item0.value), (_for_item1) => {`, + ) + expect(code).contains(`_for_item1.value+_for_item0.value`) expect(ir.template).toEqual(['', '
']) expect(ir.block.operation).toMatchObject([ { @@ -149,7 +151,7 @@ describe('compiler: v-for', () => { `
{{ id + other + index }}
`, ) expect(code).matchSnapshot() - expect(code).toContain('_getRestElement(_ctx0[0].value, ["id"])') + expect(code).toContain('_getRestElement(_for_item0.value, ["id"])') expect(ir.block.operation[0]).toMatchObject({ type: IRNodeTypes.FOR, source: { @@ -212,7 +214,7 @@ describe('compiler: v-for', () => { `
{{ id + other + index }}
`, ) expect(code).matchSnapshot() - expect(code).toContain('_ctx0[0].value.slice(1)') + expect(code).toContain('_for_item0.value.slice(1)') expect(ir.block.operation[0]).toMatchObject({ type: IRNodeTypes.FOR, source: { @@ -246,11 +248,9 @@ describe('compiler: v-for', () => { `, ) expect(code).matchSnapshot() + expect(code).toContain(`_getDefaultValue(_for_item0.value.foo, _ctx.bar)`) expect(code).toContain( - `_getDefaultValue(_ctx._ctx0[0].value.foo, _ctx.bar)`, - ) - expect(code).toContain( - `_getDefaultValue(_ctx._ctx0[0].value.baz[0], _ctx.quux)`, + `_getDefaultValue(_for_item0.value.baz[0], _ctx.quux)`, ) expect(ir.block.operation[0]).toMatchObject({ type: IRNodeTypes.FOR, diff --git a/packages/compiler-vapor/src/generators/for.ts b/packages/compiler-vapor/src/generators/for.ts index c927aa8e4a6..12d21b35151 100644 --- a/packages/compiler-vapor/src/generators/for.ts +++ b/packages/compiler-vapor/src/generators/for.ts @@ -27,11 +27,13 @@ export function genFor( const idToPathMap = parseValueDestructure() const [depth, exitScope] = context.enterScope() - const propsName = `_ctx${depth}` const idMap: Record = {} + const itemVar = `_for_item${depth}` + idMap[itemVar] = null + idToPathMap.forEach((pathInfo, id) => { - let path = `${propsName}[0].value${pathInfo ? pathInfo.path : ''}` + let path = `${itemVar}.value${pathInfo ? pathInfo.path : ''}` if (pathInfo) { if (pathInfo.helper) { idMap[pathInfo.helper] = null @@ -50,13 +52,22 @@ export function genFor( idMap[id] = path } }) - if (rawKey) idMap[rawKey] = `${propsName}[1].value` - if (rawIndex) idMap[rawIndex] = `${propsName}[2].value` - const blockFn = context.withId( - () => genBlock(render, context, [propsName]), - idMap, - ) + const args = [itemVar] + if (rawKey) { + const keyVar = `_for_key${depth}` + args.push(`, ${keyVar}`) + idMap[rawKey] = `${keyVar}.value` + idMap[keyVar] = null + } + if (rawIndex) { + const indexVar = `_for_index${depth}` + args.push(`, ${indexVar}`) + idMap[rawIndex] = `${indexVar}.value` + idMap[indexVar] = null + } + + const blockFn = context.withId(() => genBlock(render, context, args), idMap) exitScope() return [ diff --git a/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts index 03bf9c0f780..9b36a2c311f 100644 --- a/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts +++ b/packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts @@ -18,7 +18,7 @@ describe.todo('api: createSelector', () => { const isSleected = createSelector(index) return createFor( () => list.value, - ([item]) => { + item => { const span = document.createElement('li') renderEffect(() => { calledTimes += 1 @@ -73,7 +73,7 @@ describe.todo('api: createSelector', () => { ) return createFor( () => list.value, - ([item]) => { + item => { const span = document.createElement('li') renderEffect(() => { calledTimes += 1 diff --git a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts index 46aeeeeee58..96a533551c1 100644 --- a/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/templateRef.spec.ts @@ -422,7 +422,7 @@ describe('api: template ref', () => { const n1 = t0() const n2 = createFor( () => list, - state => { + item => { const n1 = t1() createTemplateRefSetter()( n1 as Element, @@ -431,7 +431,6 @@ describe('api: template ref', () => { true, ) renderEffect(() => { - const [item] = state setText(n1, item) }) return n1 @@ -485,7 +484,7 @@ describe('api: template ref', () => { const n1 = t0() const n2 = createFor( () => list, - state => { + item => { const n1 = t1() createTemplateRefSetter()( n1 as Element, @@ -494,7 +493,6 @@ describe('api: template ref', () => { true, ) renderEffect(() => { - const [item] = state setText(n1, item) }) return n1 @@ -546,7 +544,7 @@ describe('api: template ref', () => { const n2 = n1!.nextSibling! const n3 = createFor( () => list.value, - state => { + item => { const n4 = t1() createTemplateRefSetter()( n4 as Element, @@ -555,7 +553,6 @@ describe('api: template ref', () => { true, ) renderEffect(() => { - const [item] = state setText(n4, item) }) return n4 diff --git a/packages/runtime-vapor/__tests__/for.spec.ts b/packages/runtime-vapor/__tests__/for.spec.ts index 28233cbc9f5..7ba6023b1e9 100644 --- a/packages/runtime-vapor/__tests__/for.spec.ts +++ b/packages/runtime-vapor/__tests__/for.spec.ts @@ -19,14 +19,13 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => list.value, - state => { + (item, key, index) => { const span = document.createElement('li') renderEffect(() => { - const [{ value: item }, { value: key }, { value: index }] = state - span.innerHTML = `${key}. ${item.name}` + span.innerHTML = `${key.value}. ${item.value.name}` // index should be undefined if source is not an object - expect(index).toBe(undefined) + expect(index.value).toBe(undefined) }) return span }, @@ -85,11 +84,10 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => count.value, - state => { + (item, key, index) => { const span = document.createElement('li') renderEffect(() => { - const [{ value: item }, { value: key }, index] = state - span.innerHTML = `${key}. ${item}` + span.innerHTML = `${key.value}. ${item.value}` // index should be undefined if source is not an object expect(index.value).toBe(undefined) @@ -130,12 +128,11 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => data.value, - state => { + (item, key, index) => { const span = document.createElement('li') renderEffect(() => { - const [{ value: item }, { value: key }, { value: index }] = state - span.innerHTML = `${key}${index}. ${item}` - expect(index).not.toBe(undefined) + span.innerHTML = `${key.value}${index.value}. ${item.value}` + expect(index.value).not.toBe(undefined) }) return span }, @@ -197,17 +194,11 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => list.value, - state => { + (item, key, index) => { const span = document.createElement('li') renderEffect(() => { - const [ - { - value: { name }, - }, - { value: key }, - index, - ] = state - span.innerHTML = `${key}. ${name}` + // compiler rewrites { name } destructure to inline access + span.innerHTML = `${key.value}. ${item.value.name}` // index should be undefined if source is not an object expect(index.value).toBe(undefined) }) @@ -275,14 +266,14 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => list.value, - state => { + (item, _key, index) => { const span = document.createElement('li') renderEffect(() => { span.innerHTML = JSON.stringify( - getRestElement(state[0].value, ['name']), + getRestElement(item.value, ['name']), ) // index should be undefined if source is not an object - expect(state[2].value).toBe(undefined) + expect(index.value).toBe(undefined) }) return span }, @@ -341,12 +332,12 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => list.value, - state => { + (item, _key, index) => { const span = document.createElement('li') renderEffect(() => { - span.innerHTML = getDefaultValue(state[0].value.x, '0') + span.innerHTML = getDefaultValue(item.value.x, '0') // index should be undefined if source is not an object - expect(state[2].value).toBe(undefined) + expect(index.value).toBe(undefined) }) return span }, @@ -378,14 +369,13 @@ describe('createFor', () => { const { host } = define(() => { const n1 = createFor( () => list.value, - state => { + (item, key, index) => { const span = document.createElement('li') renderEffect(() => { - const [{ value: item }, { value: key }, { value: index }] = state - span.innerHTML = `${key}. ${item.name}` + span.innerHTML = `${key.value}. ${item.value.name}` // index should be undefined if source is not an object - expect(index).toBe(undefined) + expect(index.value).toBe(undefined) }) return span }, @@ -485,7 +475,7 @@ describe('createFor', () => { define(() => { const n1 = createFor( () => (++sourceCalledTimes, list.value), - ([item, index]) => { + (item, index) => { ++renderCalledTimes const span = document.createElement('li') renderEffect(() => { @@ -604,7 +594,7 @@ describe('createFor', () => { define(() => { const n1 = createFor( () => (++sourceCalledTimes, list.value), - ([item, index]) => { + (item, index) => { ++renderCalledTimes const span = document.createElement('li') renderEffect(() => { diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 50e34673fb1..8f8e62c30b3 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -15,27 +15,28 @@ import { currentInstance, isVaporComponent } from './component' import type { DynamicSlot } from './componentSlots' import { renderEffect } from './renderEffect' -type ForBlockState = [ - item: ShallowRef, - key: ShallowRef, - index: ShallowRef, -] - class ForBlock extends Fragment { scope: EffectScope | undefined - state: ForBlockState key: any + itemRef: ShallowRef + keyRef: ShallowRef | undefined + indexRef: ShallowRef | undefined + constructor( nodes: Block, scope: EffectScope | undefined, - state: ForBlockState, - key: any, + item: ShallowRef, + key: ShallowRef | undefined, + index: ShallowRef | undefined, + renderKey: any, ) { super(nodes) this.scope = scope - this.state = state - this.key = key + this.itemRef = item + this.keyRef = key + this.indexRef = index + this.key = renderKey } } @@ -50,7 +51,11 @@ type ResolvedSource = { /*! #__NO_SIDE_EFFECTS__ */ export const createFor = ( src: () => Source, - renderItem: (block: ForBlock['state']) => Block, + renderItem: ( + item: ShallowRef, + key: ShallowRef, + index: ShallowRef, + ) => Block, getKey?: (item: any, key: any, index?: number) => any, /** * Whether this v-for is used directly on a component. If true, we can avoid @@ -261,32 +266,38 @@ export const createFor = ( } } + const needKey = renderItem.length > 1 + const needIndex = renderItem.length > 2 + const mount = ( source: ResolvedSource, idx: number, anchor: Node | undefined = parentAnchor, ): ForBlock => { const [item, key, index] = getItem(source, idx) - const state = [ - shallowRef(item), - shallowRef(key), - shallowRef(index), - ] as ForBlock['state'] + const itemRef = shallowRef(item) + // avoid creating refs if the render fn doesn't need it + const keyRef = needKey ? shallowRef(key) : undefined + const indexRef = needIndex ? shallowRef(index) : undefined let nodes: Block let scope: EffectScope | undefined if (isComponent) { // component already has its own scope so no outer scope needed - nodes = renderItem(state) + nodes = renderItem(itemRef, keyRef as any, indexRef as any) } else { scope = new EffectScope() - nodes = scope.run(() => renderItem(state))! + nodes = scope.run(() => + renderItem(itemRef, keyRef as any, indexRef as any), + )! } const block = (newBlocks[idx] = new ForBlock( nodes, scope, - state, + itemRef, + keyRef, + indexRef, getKey && getKey(item, key, index), )) @@ -305,20 +316,19 @@ export const createFor = ( } const update = ( - block: ForBlock, + { itemRef, keyRef, indexRef }: ForBlock, newItem: any, - newKey = block.state[1].value, - newIndex = block.state[2].value, + newKey?: any, + newIndex?: any, ) => { - const [item, key, index] = block.state - if ( - newItem !== item.value || - newKey !== key.value || - newIndex !== index.value - ) { - item.value = newItem - key.value = newKey - index.value = newIndex + if (newIndex !== itemRef.value) { + itemRef.value = newItem + } + if (keyRef && newKey !== undefined && newKey !== keyRef.value) { + keyRef.value = newKey + } + if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) { + indexRef.value = newIndex } }