From: Evan You Date: Mon, 10 Feb 2025 18:20:53 +0000 (+0800) Subject: wip(vapor): improve node traversal codegen X-Git-Tag: v3.6.0-alpha.1~16^2~68 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=a2fa0db998177090e9480c18f873d63835905a35;p=thirdparty%2Fvuejs%2Fcore.git wip(vapor): improve node traversal codegen --- diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap index c20cba78bf..f8c5da21bc 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/transformChildren.spec.ts.snap @@ -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("

", 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("
x
", 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 +}" +`; 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 a26a0f5f01..183a3b726c 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -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 }) 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 1f5b6c6347..15abf38f99 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap @@ -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("
", 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("
", true) export function render(_ctx) { const n1 = t0() - const n0 = n1.firstChild + const n0 = _child(n1) _setProp(n0, "id", _ctx.foo) return n1 }" diff --git a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts index 78ff18ad3b..39b1da0729 100644 --- a/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/transformChildren.spec.ts @@ -16,8 +16,6 @@ const compileWithElementTransform = makeCompile({ }) describe('compiler: children transform', () => { - test.todo('basic') - test('children & sibling references', () => { const { code, helpers } = compileWithElementTransform( `
@@ -36,4 +34,16 @@ describe('compiler: children transform', () => { 'template', ]) }) + + test('efficient traversal', () => { + const { code } = compileWithElementTransform( + `
+
x
+
{{ msg }}
+
{{ msg }}
+
{{ msg }}
+
`, + ) + expect(code).toMatchSnapshot() + }) }) diff --git a/packages/compiler-vapor/src/generators/block.ts b/packages/compiler-vapor/src/generators/block.ts index 8e820dd6fc..77ba4bee8a 100644 --- a/packages/compiler-vapor/src/generators/block.ts +++ b/packages/compiler-vapor/src/generators/block.ts @@ -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)) diff --git a/packages/compiler-vapor/src/generators/template.ts b/packages/compiler-vapor/src/generators/template.ts index 936e6c8ecd..bc94a8dbfb 100644 --- a/packages/compiler-vapor/src/generators/template.ts +++ b/packages/compiler-vapor/src/generators/template.ts @@ -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 diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 361cafc0c1..395a5c964e 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -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 { diff --git a/packages/compiler-vapor/src/transforms/transformChildren.ts b/packages/compiler-vapor/src/transforms/transformChildren.ts index 3c3e885d8a..12bf743208 100644 --- a/packages/compiler-vapor/src/transforms/transformChildren.ts +++ b/packages/compiler-vapor/src/transforms/transformChildren.ts @@ -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) { diff --git a/packages/compiler-vapor/src/transforms/utils.ts b/packages/compiler-vapor/src/transforms/utils.ts index 9b7fb12728..b8e7adc605 100644 --- a/packages/compiler-vapor/src/transforms/utils.ts +++ b/packages/compiler-vapor/src/transforms/utils.ts @@ -30,6 +30,7 @@ export const newBlock = (node: BlockIRNode['node']): BlockIRNode => ({ operation: [], returns: [], expressions: [], + tempId: 0, }) export function wrapTemplate(node: ElementNode, dirs: string[]): TemplateNode { diff --git a/packages/runtime-vapor/__tests__/dom/template.spec.ts b/packages/runtime-vapor/__tests__/dom/template.spec.ts index 473104c8b8..45395e42c0 100644 --- a/packages/runtime-vapor/__tests__/dom/template.spec.ts +++ b/packages/runtime-vapor/__tests__/dom/template.spec.ts @@ -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('

') 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]) }) }) diff --git a/packages/runtime-vapor/src/dom/template.ts b/packages/runtime-vapor/src/dom/template.ts index 353ed7e363..994a2c520f 100644 --- a/packages/runtime-vapor/src/dom/template.ts +++ b/packages/runtime-vapor/src/dom/template.ts @@ -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! } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 85f1bc5eac..dd7ccfc6da 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -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,