]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): improve v-for codegen + minor optimization
authorEvan You <evan@vuejs.org>
Fri, 31 Jan 2025 05:14:16 +0000 (13:14 +0800)
committerEvan You <evan@vuejs.org>
Fri, 31 Jan 2025 05:14:16 +0000 (13:14 +0800)
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformTemplateRef.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vFor.spec.ts
packages/compiler-vapor/src/generators/for.ts
packages/runtime-vapor/__tests__/apiCreateSelector.spec.ts
packages/runtime-vapor/__tests__/dom/templateRef.spec.ts
packages/runtime-vapor/__tests__/for.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts

index da863b2407f25ca4eb8275b7e4780c993e0eb92c..302e240ca50c31e02516dd1bef3954f691f04b0c 100644 (file)
@@ -19,7 +19,7 @@ const t0 = _template("<div></div>", 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
index 694b804cd4b2a7259f9786c2246386fc971f5312..21b4b1d4a39c4667ee78b95afa1dedb6ecf86025 100644 (file)
@@ -5,9 +5,9 @@ exports[`compiler: v-for > array de-structured value (with rest) 1`] = `
 const t0 = _template("<div></div>", 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("<div></div>", 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("<div></div>", 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("<div></div>", 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("<span></span>")
 const t1 = _template("<div></div>", 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("<div></div>", 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("<span></span>", 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("<div></div>", 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("<div>item</div>", true)
 
 export function render(_ctx) {
-  const n0 = _createFor(() => (_ctx.items), (_ctx0) => {
+  const n0 = _createFor(() => (_ctx.items), (_for_item0) => {
     const n2 = t0()
     return n2
   })
index 15fcd2182a8a4343cb8e90e8f2dbfd650e5b23d5..1f5b6c6347680ca98d1747c34f0ceb8f6e9cdd60 100644 (file)
@@ -65,7 +65,7 @@ exports[`compiler: v-once > with v-for 1`] = `
 const t0 = _template("<div></div>", 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)
index 0e2a7bd826ee01f578c04340fd2ed0d1e35b4143..22ded75aeb98a3a2e174cd1a124950fe5c019007 100644 (file)
@@ -84,9 +84,11 @@ describe('compiler: v-for', () => {
       `<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].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(['<span></span>', '<div></div>'])
     expect(ir.block.operation).toMatchObject([
       {
@@ -149,7 +151,7 @@ describe('compiler: v-for', () => {
       `<div v-for="(  { id, ...other }, index) in list" :key="id">{{ id + other + index }}</div>`,
     )
     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', () => {
       `<div v-for="([id, ...other], index) in list" :key="id">{{ id + other + index }}</div>`,
     )
     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', () => {
       </div>`,
     )
     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,
index c927aa8e4a63e864a367ac9fd8e7ba89f5aaee3d..12d21b351518706f6124193efb4a8f7527b5375d 100644 (file)
@@ -27,11 +27,13 @@ export function genFor(
   const idToPathMap = parseValueDestructure()
 
   const [depth, exitScope] = context.enterScope()
-  const propsName = `_ctx${depth}`
   const idMap: Record<string, string | SimpleExpressionNode | null> = {}
 
+  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 [
index 03bf9c0f780909a8327b935d28b1d1801ca187ad..9b36a2c311fd83b417d9508f4235eac626e81ca5 100644 (file)
@@ -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
index 46aeeeeee585c16916d1b51ccef4b663ac892fca..96a533551c1bdb05ee7c15b4a2afce2449c80717 100644 (file)
@@ -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
index 28233cbc9f53a10c66b1cfc6f72f14fd1ac8fa38..7ba6023b1e97a2d1a9945d175111413fd53c10d6 100644 (file)
@@ -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(() => {
index 50e34673fb12fbc369e89412044de6594596dfc9..8f8e62c30b3c1298cc9ba3a868c39893f4ccfba5 100644 (file)
@@ -15,27 +15,28 @@ import { currentInstance, isVaporComponent } from './component'
 import type { DynamicSlot } from './componentSlots'
 import { renderEffect } from './renderEffect'
 
-type ForBlockState = [
-  item: ShallowRef<any>,
-  key: ShallowRef<any>,
-  index: ShallowRef<number | undefined>,
-]
-
 class ForBlock extends Fragment {
   scope: EffectScope | undefined
-  state: ForBlockState
   key: any
 
+  itemRef: ShallowRef<any>
+  keyRef: ShallowRef<any> | undefined
+  indexRef: ShallowRef<number | undefined> | undefined
+
   constructor(
     nodes: Block,
     scope: EffectScope | undefined,
-    state: ForBlockState,
-    key: any,
+    item: ShallowRef<any>,
+    key: ShallowRef<any> | undefined,
+    index: ShallowRef<number | undefined> | 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<any>,
+    key: ShallowRef<any>,
+    index: ShallowRef<number | undefined>,
+  ) => 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
     }
   }