Skip to content

Commit 3c22e74

Browse files
committed
feat(compiler): generate more efficient runtime code for specific interpolation patterns
Copied from vuejs/core#13278
1 parent 9dcb0cc commit 3c22e74

File tree

8 files changed

+592
-158
lines changed

8 files changed

+592
-158
lines changed

docs/introduction/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ We assume you are already familiar with the basic usages of Vue before you conti
2525
pnpm add vue-jsx-vapor
2626

2727
# runtime
28-
pnpm add https://pkg.pr.new/vue@51677cd
28+
pnpm add https://pkg.pr.new/vue@73bceb2
2929
```
3030

3131
The Vue Vapor runtime is not release, so we use [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) to install.

packages/compiler/src/generators/block.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ export function genBlock(
1919
context: CodegenContext,
2020
args: CodeFragment[] = [],
2121
root?: boolean,
22-
customReturns?: (returns: CodeFragment[]) => CodeFragment[],
2322
): CodeFragment[] {
2423
return [
2524
'(',
2625
...args,
2726
') => {',
2827
INDENT_START,
29-
...genBlockContent(oper, context, root, customReturns),
28+
...genBlockContent(oper, context, root),
3029
INDENT_END,
3130
NEWLINE,
3231
'}',
@@ -37,7 +36,7 @@ export function genBlockContent(
3736
block: BlockIRNode,
3837
context: CodegenContext,
3938
root?: boolean,
40-
customReturns?: (returns: CodeFragment[]) => CodeFragment[],
39+
genEffectsExtraFrag?: () => CodeFragment[],
4140
): CodeFragment[] {
4241
const [frag, push] = buildCodeFragment()
4342
const { dynamic, effect, operation, returns } = block
@@ -70,7 +69,7 @@ export function genBlockContent(
7069
}
7170

7271
push(...genOperations(operation, context))
73-
push(...genEffects(effect, context))
72+
push(...genEffects(effect, context, genEffectsExtraFrag))
7473

7574
push(NEWLINE, `return `)
7675

@@ -79,7 +78,7 @@ export function genBlockContent(
7978
returnNodes.length > 1
8079
? genMulti(DELIMITERS_ARRAY, ...returnNodes)
8180
: [returnNodes[0] || 'null']
82-
push(...(customReturns ? customReturns(returnsCode) : returnsCode))
81+
push(...returnsCode)
8382

8483
resetBlock()
8584
return frag

packages/compiler/src/generators/for.ts

Lines changed: 301 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
11
import { parseExpression } from '@babel/parser'
2+
import {
3+
isNodesEquivalent,
4+
type Expression,
5+
type Identifier,
6+
type Node,
7+
} from '@babel/types'
28
import {
39
createSimpleExpression,
10+
isStaticNode,
411
walkIdentifiers,
512
type SimpleExpressionNode,
613
} from '@vue/compiler-dom'
14+
import { extend, isGloballyAllowed } from '@vue/shared'
15+
import { walkAST } from 'ast-kit'
716
import type { CodegenContext } from '../generate'
8-
import type { ForIRNode } from '../ir'
9-
import { genBlock } from './block'
17+
import type { BlockIRNode, ForIRNode, IREffect } from '../ir'
18+
import { genBlockContent } from './block'
1019
import { genExpression } from './expression'
11-
import { genCall, genMulti, NEWLINE, type CodeFragment } from './utils'
12-
import type { Identifier } from '@babel/types'
20+
import { genOperation } from './operation'
21+
import {
22+
genCall,
23+
genMulti,
24+
INDENT_END,
25+
INDENT_START,
26+
NEWLINE,
27+
type CodeFragment,
28+
} from './utils'
1329

1430
/**
1531
* Flags to optimize vapor `createFor` runtime behavior, shared between the
@@ -98,7 +114,60 @@ export function genFor(
98114
idMap[indexVar] = null
99115
}
100116

101-
const blockFn = context.withId(() => genBlock(render, context, args), idMap)
117+
const { selectorPatterns, keyOnlyBindingPatterns } = matchPatterns(
118+
render,
119+
keyProp,
120+
idMap,
121+
)
122+
const patternFrag: CodeFragment[] = []
123+
124+
for (const [i, { selector }] of selectorPatterns.entries()) {
125+
const selectorName = `_selector${id}_${i}`
126+
patternFrag.push(
127+
NEWLINE,
128+
`const ${selectorName} = `,
129+
...genCall(`n${id}.useSelector`, [
130+
`() => `,
131+
...genExpression(selector, context),
132+
]),
133+
)
134+
}
135+
136+
const blockFn = context.withId(() => {
137+
const frag: CodeFragment[] = []
138+
frag.push('(', ...args, ') => {', INDENT_START)
139+
if (selectorPatterns.length || keyOnlyBindingPatterns.length) {
140+
frag.push(
141+
...genBlockContent(render, context, false, () => {
142+
const patternFrag: CodeFragment[] = []
143+
144+
for (const [i, { effect }] of selectorPatterns.entries()) {
145+
patternFrag.push(
146+
NEWLINE,
147+
`_selector${id}_${i}(() => {`,
148+
INDENT_START,
149+
)
150+
for (const oper of effect.operations) {
151+
patternFrag.push(...genOperation(oper, context))
152+
}
153+
patternFrag.push(INDENT_END, NEWLINE, `})`)
154+
}
155+
156+
for (const { effect } of keyOnlyBindingPatterns) {
157+
for (const oper of effect.operations) {
158+
patternFrag.push(...genOperation(oper, context))
159+
}
160+
}
161+
162+
return patternFrag
163+
}),
164+
)
165+
} else {
166+
frag.push(...genBlockContent(render, context))
167+
}
168+
frag.push(INDENT_END, NEWLINE, '}')
169+
return frag
170+
}, idMap)
102171
exitScope()
103172

104173
let flags = 0
@@ -123,6 +192,7 @@ export function genFor(
123192
flags ? String(flags) : undefined,
124193
// todo: hydrationNode
125194
),
195+
...patternFrag,
126196
]
127197

128198
// construct a id -> accessor path map.
@@ -251,3 +321,229 @@ export function genFor(
251321
return idMap
252322
}
253323
}
324+
325+
function matchPatterns(
326+
render: BlockIRNode,
327+
keyProp: SimpleExpressionNode | undefined,
328+
idMap: Record<string, string | SimpleExpressionNode | null>,
329+
) {
330+
const selectorPatterns: NonNullable<
331+
ReturnType<typeof matchSelectorPattern>
332+
>[] = []
333+
const keyOnlyBindingPatterns: NonNullable<
334+
ReturnType<typeof matchKeyOnlyBindingPattern>
335+
>[] = []
336+
337+
render.effect = render.effect.filter((effect) => {
338+
if (keyProp !== undefined) {
339+
const selector = matchSelectorPattern(effect, keyProp.ast, idMap)
340+
if (selector) {
341+
selectorPatterns.push(selector)
342+
return false
343+
}
344+
const keyOnly = matchKeyOnlyBindingPattern(effect, keyProp.ast)
345+
if (keyOnly) {
346+
keyOnlyBindingPatterns.push(keyOnly)
347+
return false
348+
}
349+
}
350+
351+
return true
352+
})
353+
354+
return {
355+
keyOnlyBindingPatterns,
356+
selectorPatterns,
357+
}
358+
}
359+
360+
function matchKeyOnlyBindingPattern(
361+
effect: IREffect,
362+
keyAst: any,
363+
):
364+
| {
365+
effect: IREffect
366+
}
367+
| undefined {
368+
// TODO: expressions can be multiple?
369+
if (effect.expressions.length === 1) {
370+
const ast = effect.expressions[0].ast
371+
if (
372+
typeof ast === 'object' &&
373+
ast !== null &&
374+
isKeyOnlyBinding(ast, keyAst)
375+
) {
376+
return { effect }
377+
}
378+
}
379+
}
380+
381+
function matchSelectorPattern(
382+
effect: IREffect,
383+
keyAst: any,
384+
idMap: Record<string, string | SimpleExpressionNode | null>,
385+
):
386+
| {
387+
effect: IREffect
388+
selector: SimpleExpressionNode
389+
}
390+
| undefined {
391+
// TODO: expressions can be multiple?
392+
if (effect.expressions.length === 1) {
393+
const ast = effect.expressions[0].ast
394+
const offset = effect.expressions[0].loc.start.offset
395+
if (typeof ast === 'object' && ast) {
396+
const matcheds: [key: Expression, selector: Expression][] = []
397+
398+
walkAST(ast, {
399+
enter(node) {
400+
if (
401+
typeof node === 'object' &&
402+
node &&
403+
node.type === 'BinaryExpression' &&
404+
node.operator === '===' &&
405+
node.left.type !== 'PrivateName'
406+
) {
407+
const { left, right } = node
408+
for (const [a, b] of [
409+
[left, right],
410+
[right, left],
411+
]) {
412+
const aIsKey = isKeyOnlyBinding(a, keyAst)
413+
const bIsKey = isKeyOnlyBinding(b, keyAst)
414+
const bVars = analyzeVariableScopes(b, idMap)
415+
if (aIsKey && !bIsKey && !bVars.locals.length) {
416+
matcheds.push([a, b])
417+
}
418+
}
419+
}
420+
},
421+
})
422+
423+
if (matcheds.length === 1) {
424+
const [key, selector] = matcheds[0]
425+
const content = effect.expressions[0].content
426+
427+
let hasExtraId = false
428+
const parentStackMap = new Map<Identifier, Node[]>()
429+
const parentStack: Node[] = []
430+
walkIdentifiers(
431+
ast,
432+
(id) => {
433+
if (id.start !== key.start && id.start !== selector.start) {
434+
hasExtraId = true
435+
}
436+
parentStackMap.set(id, parentStack.slice())
437+
},
438+
false,
439+
parentStack,
440+
)
441+
442+
if (!hasExtraId) {
443+
const name = content.slice(
444+
selector.start! - offset,
445+
selector.end! - offset,
446+
)
447+
return {
448+
effect,
449+
// @ts-expect-error
450+
selector: {
451+
content: name,
452+
ast: extend({}, selector, {
453+
start: 1,
454+
end: name.length + 1,
455+
}),
456+
loc: selector.loc as any,
457+
isStatic: false,
458+
},
459+
}
460+
}
461+
}
462+
}
463+
464+
const content = effect.expressions[0].content
465+
if (
466+
typeof ast === 'object' &&
467+
ast &&
468+
ast.type === 'ConditionalExpression' &&
469+
ast.test.type === 'BinaryExpression' &&
470+
ast.test.operator === '===' &&
471+
ast.test.left.type !== 'PrivateName' &&
472+
isStaticNode(ast.consequent) &&
473+
isStaticNode(ast.alternate)
474+
) {
475+
const left = ast.test.left
476+
const right = ast.test.right
477+
for (const [a, b] of [
478+
[left, right],
479+
[right, left],
480+
]) {
481+
const aIsKey = isKeyOnlyBinding(a, keyAst)
482+
const bIsKey = isKeyOnlyBinding(b, keyAst)
483+
const bVars = analyzeVariableScopes(b, idMap)
484+
if (aIsKey && !bIsKey && !bVars.locals.length) {
485+
return {
486+
effect,
487+
// @ts-expect-error
488+
selector: {
489+
content: content.slice(b.start! - offset, b.end! - offset),
490+
ast: b,
491+
loc: b.loc as any,
492+
isStatic: false,
493+
},
494+
}
495+
}
496+
}
497+
}
498+
}
499+
}
500+
501+
function analyzeVariableScopes(
502+
ast: Node,
503+
idMap: Record<string, string | SimpleExpressionNode | null>,
504+
) {
505+
const globals: string[] = []
506+
const locals: string[] = []
507+
508+
const ids: Identifier[] = []
509+
const parentStackMap = new Map<Identifier, Node[]>()
510+
const parentStack: Node[] = []
511+
walkIdentifiers(
512+
ast,
513+
(id) => {
514+
ids.push(id)
515+
parentStackMap.set(id, parentStack.slice())
516+
},
517+
false,
518+
parentStack,
519+
)
520+
521+
for (const id of ids) {
522+
if (isGloballyAllowed(id.name)) {
523+
continue
524+
}
525+
if (idMap[id.name]) {
526+
locals.push(id.name)
527+
} else {
528+
globals.push(id.name)
529+
}
530+
}
531+
532+
return { globals, locals }
533+
}
534+
535+
function isKeyOnlyBinding(expr: Node, keyAst: any) {
536+
let only = true
537+
walkAST(expr, {
538+
enter(node) {
539+
if (isNodesEquivalent(node, keyAst)) {
540+
this.skip()
541+
return
542+
}
543+
if (node.type === 'Identifier') {
544+
only = false
545+
}
546+
},
547+
})
548+
return only
549+
}

0 commit comments

Comments
 (0)