]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): improve node traversal codegen
authorEvan You <evan@vuejs.org>
Mon, 10 Feb 2025 18:20:53 +0000 (02:20 +0800)
committerEvan You <evan@vuejs.org>
Mon, 10 Feb 2025 18:21:59 +0000 (02:21 +0800)
12 files changed:
packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.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/transformChildren.spec.ts
packages/compiler-vapor/src/generators/block.ts
packages/compiler-vapor/src/generators/template.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformChildren.ts
packages/compiler-vapor/src/transforms/utils.ts
packages/runtime-vapor/__tests__/dom/template.spec.ts
packages/runtime-vapor/src/dom/template.ts
packages/runtime-vapor/src/index.ts

index c20cba78bf38b8efaf96405252761b9bf1868ecb..f8c5da21bc0b6a28e9813b45e683c71a1d681eb2 100644 (file)
@@ -1,14 +1,14 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
 exports[`compiler: children transform > children & sibling references 1`] = `
-"import { next as _next, createTextNode as _createTextNode, insert as _insert, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+"import { child as _child, nextn as _nextn, next as _next, createTextNode as _createTextNode, insert as _insert, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
 const t0 = _template("<div><p></p> <!><p></p></div>", true)
 
 export function render(_ctx) {
   const n4 = t0()
-  const n0 = n4.firstChild
-  const n3 = _next(n0, 2)
-  const n2 = n3.nextSibling
+  const n0 = _child(n4)
+  const n3 = _nextn(n0, 2)
+  const n2 = _next(n3)
   const n1 = _createTextNode(() => [_ctx.second, " ", _ctx.third, " "])
   _insert(n1, n4, n3)
   _renderEffect(() => {
@@ -18,3 +18,26 @@ export function render(_ctx) {
   return n4
 }"
 `;
+
+exports[`compiler: children transform > efficient traversal 1`] = `
+"import { child as _child, next as _next, setText as _setText, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<div><div>x</div><div><span></span></div><div><span></span></div><div><span></span></div></div>", true)
+
+export function render(_ctx) {
+  const n3 = t0()
+  const _n0 = _next(_child(n3))
+  const n0 = _child(_n0)
+  const _n1 = _next(_n0)
+  const n1 = _child(_n1)
+  const _n2 = _next(_n1)
+  const n2 = _child(_n2)
+  _renderEffect(() => {
+    const _msg = _ctx.msg
+    
+    _setText(n0, _msg)
+    _setText(n1, _msg)
+    _setText(n2, _msg)
+  })
+  return n3
+}"
+`;
index a26a0f5f018571513d16c01627e762675f546887..183a3b726c16ec0aa99b393b7ec3c0f262612fd4 100644 (file)
@@ -73,7 +73,7 @@ export function render(_ctx) {
       const n4 = t0()
       _renderEffect(() => _setText(n4, _for_item1.value+_for_item0.value))
       return n4
-    })
+    }, null, null, null, true)
     _insert(n2, n5)
     return n5
   })
index 1f5b6c6347680ca98d1747c34f0ceb8f6e9cdd60..15abf38f9992e4a37153d391b5ba9e5d3634e19f 100644 (file)
@@ -12,12 +12,12 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-once > basic 1`] = `
-"import { createTextNode as _createTextNode, setClass as _setClass, prepend as _prepend, template as _template } from 'vue';
+"import { child as _child, createTextNode as _createTextNode, setClass as _setClass, prepend as _prepend, template as _template } from 'vue';
 const t0 = _template("<div><span></span></div>", true)
 
 export function render(_ctx, $props, $emit, $attrs, $slots) {
   const n2 = t0()
-  const n1 = n2.firstChild
+  const n1 = _child(n2)
   const n0 = _createTextNode([_ctx.msg, " "])
   _setClass(n1, _ctx.clz)
   _prepend(n2, n0)
@@ -49,12 +49,12 @@ export function render(_ctx) {
 `;
 
 exports[`compiler: v-once > on nested plain element 1`] = `
-"import { setProp as _setProp, template as _template } from 'vue';
+"import { child as _child, setProp as _setProp, template as _template } from 'vue';
 const t0 = _template("<div><div></div></div>", true)
 
 export function render(_ctx) {
   const n1 = t0()
-  const n0 = n1.firstChild
+  const n0 = _child(n1)
   _setProp(n0, "id", _ctx.foo)
   return n1
 }"
index 78ff18ad3b62ad22a183cbb7544217d834154d12..39b1da072900df1976665d38bf1c2e55a392c3f3 100644 (file)
@@ -16,8 +16,6 @@ const compileWithElementTransform = makeCompile({
 })
 
 describe('compiler: children transform', () => {
-  test.todo('basic')
-
   test('children & sibling references', () => {
     const { code, helpers } = compileWithElementTransform(
       `<div>
@@ -36,4 +34,16 @@ describe('compiler: children transform', () => {
       'template',
     ])
   })
+
+  test('efficient traversal', () => {
+    const { code } = compileWithElementTransform(
+      `<div>
+    <div>x</div>
+    <div><span>{{ msg }}</span></div>
+    <div><span>{{ msg }}</span></div>
+    <div><span>{{ msg }}</span></div>
+  </div>`,
+    )
+    expect(code).toMatchSnapshot()
+  })
 })
index 8e820dd6fce3e8b82e0d4fac5682556d63ddfb64..77ba4bee8a7c087b22760989e2745771ab051831 100644 (file)
@@ -49,7 +49,7 @@ export function genBlockContent(
   }
 
   for (const child of dynamic.children) {
-    push(...genChildren(child, context, child.id!))
+    push(...genChildren(child, context, `n${child.id!}`))
   }
 
   push(...genOperations(operation, context))
index 936e6c8ecd8b1b2bcbe50c4818dbe60122ad9942..bc94a8dbfb290ed2a447e3a94a229c081cba2f2b 100644 (file)
@@ -21,8 +21,9 @@ export function genTemplates(
 export function genChildren(
   dynamic: IRDynamicInfo,
   context: CodegenContext,
-  from: number,
-  paths: number[] = [],
+  from: string,
+  path: number[] = [],
+  knownPaths: [id: string, path: number[]][] = [],
 ): CodeFragment[] {
   const { helper } = context
   const [frag, push] = buildCodeFragment()
@@ -34,7 +35,7 @@ export function genChildren(
     push(...genDirectivesForElement(id, context))
   }
 
-  let prev: [id: number, elementIndex: number] | undefined
+  let prev: [variable: string, elementIndex: number] | undefined
   for (const [index, child] of children.entries()) {
     if (child.flags & DynamicFlag.NON_TEMPLATE) {
       offset--
@@ -47,34 +48,81 @@ export function genChildren(
           : child.id
         : undefined
 
-    const elementIndex = Number(index) + offset
-    const newPaths = [...paths, elementIndex]
-
-    if (id === undefined) {
-      push(...genChildren(child, context, from, newPaths))
+    if (id === undefined && !child.hasDynamicChild) {
+      const { id, template } = child
+      if (id !== undefined && template !== undefined) {
+        push(NEWLINE, `const n${id} = t${template}()`)
+        push(...genDirectivesForElement(id, context))
+      }
       continue
     }
 
-    push(NEWLINE, `const n${id} = `)
+    const elementIndex = Number(index) + offset
+    const newPath = [...path, elementIndex]
+
+    const variable = id === undefined ? `_n${context.block.tempId++}` : `n${id}`
+    push(NEWLINE, `const ${variable} = `)
+
     if (prev) {
       const offset = elementIndex - prev[1]
       if (offset === 1) {
-        push(`n${prev[0]}.nextSibling`)
+        push(...genCall(helper('next'), prev[0]))
       } else {
-        push(...genCall(helper('next'), `n${prev[0]}`, String(offset)))
+        push(...genCall(helper('nextn'), prev[0], String(offset)))
       }
     } else {
-      if (newPaths.length === 1 && newPaths[0] === 0) {
-        push(`n${from}.firstChild`)
+      if (newPath.length === 1 && newPath[0] === 0) {
+        push(...genCall(helper('child'), from))
       } else {
-        push(
-          ...genCall(helper('children'), `n${from}`, ...newPaths.map(String)),
-        )
+        // check if there's a node that we can reuse from
+        let resolvedFrom = from
+        let resolvedPath = newPath
+        let skipFirstChild = false
+        outer: for (const [from, path] of knownPaths) {
+          const l = path.length
+          const tail = newPath.slice(l)
+          for (let i = 0; i < l; i++) {
+            const parentSeg = path[i]
+            const thisSeg = newPath[i]
+            if (parentSeg !== thisSeg) {
+              if (i === l - 1) {
+                // last bit is reusable
+                resolvedFrom = from
+                resolvedPath = [thisSeg - parentSeg, ...tail]
+                skipFirstChild = true
+                break outer
+              }
+              break
+            } else if (i === l - 1) {
+              // full overlap
+              resolvedFrom = from
+              resolvedPath = tail
+              break outer
+            }
+          }
+        }
+        let init
+        for (const i of resolvedPath) {
+          init = init
+            ? genCall(helper('child'), init)
+            : skipFirstChild
+              ? resolvedFrom
+              : genCall(helper('child'), resolvedFrom)
+          if (i === 1) {
+            init = genCall(helper('next'), init)
+          } else if (i > 1) {
+            init = genCall(helper('nextn'), init, String(i))
+          }
+        }
+        push(...init!)
       }
     }
-    push(...genDirectivesForElement(id, context))
-    prev = [id, elementIndex]
-    push(...genChildren(child, context, id, []))
+    if (id !== undefined) {
+      push(...genDirectivesForElement(id, context))
+    }
+    knownPaths.unshift([variable, newPath])
+    prev = [variable, elementIndex]
+    push(...genChildren(child, context, variable))
   }
 
   return frag
index 361cafc0c17c59e7a4ebc70cc4636b0c34066242..395a5c964ecdfb6a0caeb626395570f418db50b1 100644 (file)
@@ -48,6 +48,7 @@ export interface BlockIRNode extends BaseIRNode {
   type: IRNodeTypes.BLOCK
   node: RootNode | TemplateChildNode
   dynamic: IRDynamicInfo
+  tempId: number
   effect: IREffect[]
   operation: OperationNode[]
   expressions: SimpleExpressionNode[]
@@ -249,6 +250,7 @@ export interface IRDynamicInfo {
   anchor?: number
   children: IRDynamicInfo[]
   template?: number
+  hasDynamicChild?: boolean
 }
 
 export interface IREffect {
index 3c3e885d8ac617791588cc0818b7382fb595aa21..12bf743208e9942896c74d822fb122cf03ee04a1 100644 (file)
@@ -19,13 +19,15 @@ export const transformChildren: NodeTransform = (node, context) => {
     const childContext = context.create(child, i)
     transformNode(childContext)
 
+    const childDynamic = childContext.dynamic
+
     if (isFragment) {
       childContext.reference()
       childContext.registerTemplate()
 
       if (
-        !(childContext.dynamic.flags & DynamicFlag.NON_TEMPLATE) ||
-        childContext.dynamic.flags & DynamicFlag.INSERT
+        !(childDynamic.flags & DynamicFlag.NON_TEMPLATE) ||
+        childDynamic.flags & DynamicFlag.INSERT
       ) {
         context.block.returns.push(childContext.dynamic.id!)
       }
@@ -33,7 +35,16 @@ export const transformChildren: NodeTransform = (node, context) => {
       context.childrenTemplate.push(childContext.template)
     }
 
-    context.dynamic.children[i] = childContext.dynamic
+    if (
+      childDynamic.hasDynamicChild ||
+      childDynamic.id !== undefined ||
+      childDynamic.flags & DynamicFlag.NON_TEMPLATE ||
+      childDynamic.flags & DynamicFlag.INSERT
+    ) {
+      context.dynamic.hasDynamicChild = true
+    }
+
+    context.dynamic.children[i] = childDynamic
   }
 
   if (!isFragment) {
index 9b7fb127288f873129ed3a759b4613e0d32ccce0..b8e7adc60596dace721318181395ba43abf35c44 100644 (file)
@@ -30,6 +30,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({
   operation: [],
   returns: [],
   expressions: [],
+  tempId: 0,
 })
 
 export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode {
index 473104c8b8939408791934df92a8b3753890c1e0..45395e42c0e15b4f84005fd3905f5fbdb0c4d3dc 100644 (file)
@@ -1,4 +1,4 @@
-import { children, next, template } from '../../src/dom/template'
+import { children, next, nextn, template } from '../../src/dom/template'
 
 describe('api: template', () => {
   test('create element', () => {
@@ -32,11 +32,11 @@ describe('api: template', () => {
     const t = template('<div><span></span><b></b><p></p></div>')
     const root = t()
     const span = children(root, 0)
-    const b = next(span, 1)
+    const b = next(span)
 
     expect(span).toBe(root.childNodes[0])
     expect(b).toBe(root.childNodes[1])
-    expect(next(span, 2)).toBe(root.childNodes[2])
-    expect(next(b, 1)).toBe(root.childNodes[2])
+    expect(nextn(span, 2)).toBe(root.childNodes[2])
+    expect(next(b)).toBe(root.childNodes[2])
   })
 })
index 353ed7e363e1582cb4bda55ab23a1c083916b0a6..994a2c520f48e9b8047bf2683dbc5ee51dac42d5 100644 (file)
@@ -28,8 +28,15 @@ export function children(node: Node, ...paths: number[]): Node {
   return node
 }
 
-/*! #__NO_SIDE_EFFECTS__ */
-export function next(node: Node, offset: number): Node {
+export function child(node: ParentNode): Node {
+  return node.firstChild!
+}
+
+export function next(node: Node): Node {
+  return node.nextSibling!
+}
+
+export function nextn(node: Node, offset: number = 1): Node {
   for (let i = 0; i < offset; i++) {
     node = node.nextSibling!
   }
index 85f1bc5eacee3380ca7bccdfa1fbf56860773a44..dd7ccfc6daa312129faeb7fd7df045b6e14adc80 100644 (file)
@@ -9,7 +9,7 @@ export { insert, prepend, remove } from './block'
 export { createComponent, createComponentWithFallback } from './component'
 export { renderEffect } from './renderEffect'
 export { createSlot } from './componentSlots'
-export { template, children, next } from './dom/template'
+export { template, children, child, next, nextn } from './dom/template'
 export { createTextNode } from './dom/node'
 export {
   setText,