Skip to content

Commit 58593c4

Browse files
committed
feat(v-on): cache handlers
1 parent 39ea67a commit 58593c4

File tree

19 files changed

+529
-243
lines changed

19 files changed

+529
-243
lines changed

packages/compiler-core/__tests__/codegen.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
createCallExpression,
1414
createConditionalExpression,
1515
IfCodegenNode,
16-
ForCodegenNode
16+
ForCodegenNode,
17+
createCacheExpression
1718
} from '../src'
1819
import {
1920
CREATE_VNODE,
@@ -34,6 +35,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
3435
components: [],
3536
directives: [],
3637
hoists: [],
38+
cached: 0,
3739
codegenNode: createSimpleExpression(`null`, false),
3840
loc: locStub,
3941
...options
@@ -135,6 +137,12 @@ describe('compiler: codegen', () => {
135137
expect(code).toMatchSnapshot()
136138
})
137139

140+
test('cached', () => {
141+
const root = createRoot({ cached: 3 })
142+
const { code } = generate(root)
143+
expect(code).toMatch(`let _cached_1, _cached_2, _cached_3`)
144+
})
145+
138146
test('prefixIdentifiers: true should inject _ctx statement', () => {
139147
const { code } = generate(createRoot(), { prefixIdentifiers: true })
140148
expect(code).toMatch(`const _ctx = this\n`)
@@ -359,4 +367,16 @@ describe('compiler: codegen', () => {
359367
)
360368
expect(code).toMatchSnapshot()
361369
})
370+
371+
test('CacheExpression', () => {
372+
const { code } = generate(
373+
createRoot({
374+
codegenNode: createCacheExpression(
375+
1,
376+
createSimpleExpression(`foo`, false)
377+
)
378+
})
379+
)
380+
expect(code).toMatch(`_cached_1 || (_cached_1 = foo)`)
381+
})
362382
})

packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,24 @@ return function render() {
203203
}"
204204
`;
205205
206+
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers 1`] = `
207+
"const _Vue = Vue
208+
209+
let _cached_1
210+
211+
return function render() {
212+
with (this) {
213+
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
214+
215+
return (_openBlock(), _createBlock(\\"div\\", null, [
216+
_createVNode(\\"div\\", {
217+
onClick: _cached_1 || (_cached_1 = $event => (_ctx.foo($event)))
218+
})
219+
]))
220+
}
221+
}"
222+
`;
223+
206224
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (2) 1`] = `
207225
"const _Vue = Vue
208226

packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function render() {
88
return (openBlock(), createBlock(\\"input\\", {
99
modelValue: _ctx.model[_ctx.index],
1010
\\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event)
11-
}, null, 8 /* PROPS */, [\\"modelValue\\"]))
11+
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
1212
}"
1313
`;
1414

@@ -35,7 +35,7 @@ export default function render() {
3535
return (openBlock(), createBlock(\\"input\\", {
3636
modelValue: _ctx.model,
3737
\\"onUpdate:modelValue\\": $event => (_ctx.model = $event)
38-
}, null, 8 /* PROPS */, [\\"modelValue\\"]))
38+
}, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
3939
}"
4040
`;
4141

packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,25 @@ import { transformExpression } from '../../src/transforms/transformExpression'
1818
import { transformIf } from '../../src/transforms/vIf'
1919
import { transformFor } from '../../src/transforms/vFor'
2020
import { transformBind } from '../../src/transforms/vBind'
21+
import { transformOn } from '../../src/transforms/vOn'
2122
import { createObjectMatcher, genFlagText } from '../testUtils'
2223
import { PatchFlags } from '@vue/shared'
2324

2425
function transformWithHoist(template: string, options: CompilerOptions = {}) {
2526
const ast = parse(template)
2627
transform(ast, {
2728
hoistStatic: true,
28-
prefixIdentifiers: options.prefixIdentifiers,
2929
nodeTransforms: [
3030
transformIf,
3131
transformFor,
3232
...(options.prefixIdentifiers ? [transformExpression] : []),
3333
transformElement
3434
],
3535
directiveTransforms: {
36+
on: transformOn,
3637
bind: transformBind
37-
}
38+
},
39+
...options
3840
})
3941
expect(ast.codegenNode).toMatchObject({
4042
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
@@ -656,5 +658,16 @@ describe('compiler: hoistStatic transform', () => {
656658
expect(root.hoists.length).toBe(0)
657659
expect(generate(root).code).toMatchSnapshot()
658660
})
661+
662+
test('should NOT hoist elements with cached handlers', () => {
663+
const { root } = transformWithHoist(`<div><div @click="foo"/></div>`, {
664+
prefixIdentifiers: true,
665+
cacheHandlers: true
666+
})
667+
668+
expect(root.cached).toBe(1)
669+
expect(root.hoists.length).toBe(0)
670+
expect(generate(root).code).toMatchSnapshot()
671+
})
659672
})
660673
})

packages/compiler-core/__tests__/transforms/vModel.spec.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
ForNode,
1010
PlainElementNode,
1111
PlainElementCodegenNode,
12-
ComponentNode
12+
ComponentNode,
13+
NodeTypes
1314
} from '../../src'
1415
import { ErrorCodes } from '../../src/errors'
1516
import { transformModel } from '../../src/transforms/vModel'
@@ -338,25 +339,36 @@ describe('compiler: transform v-model', () => {
338339
expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
339340
})
340341

341-
test('should not mark update handler dynamic', () => {
342+
test('should cache update handler w/ cacheHandlers: true', () => {
342343
const root = parseWithVModel('<input v-model="foo" />', {
343-
prefixIdentifiers: true
344+
prefixIdentifiers: true,
345+
cacheHandlers: true
344346
})
347+
expect(root.cached).toBe(1)
345348
const codegen = (root.children[0] as PlainElementNode)
346349
.codegenNode as PlainElementCodegenNode
350+
// should not list cached prop in dynamicProps
347351
expect(codegen.arguments[4]).toBe(`["modelValue"]`)
352+
expect(
353+
(codegen.arguments[1] as ObjectExpression).properties[1].value.type
354+
).toBe(NodeTypes.JS_CACHE_EXPRESSION)
348355
})
349356

350-
test('should mark update handler dynamic if it refers v-for scope variables', () => {
357+
test('should not cache update handler if it refers v-for scope variables', () => {
351358
const root = parseWithVModel(
352359
'<input v-for="i in list" v-model="foo[i]" />',
353360
{
354-
prefixIdentifiers: true
361+
prefixIdentifiers: true,
362+
cacheHandlers: true
355363
}
356364
)
365+
expect(root.cached).toBe(0)
357366
const codegen = ((root.children[0] as ForNode)
358367
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode
359368
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
369+
expect(
370+
(codegen.arguments[1] as ObjectExpression).properties[1].value.type
371+
).not.toBe(NodeTypes.JS_CACHE_EXPRESSION)
360372
})
361373

362374
test('should mark update handler dynamic if it refers slot scope variables', () => {
@@ -389,7 +401,7 @@ describe('compiler: transform v-model', () => {
389401
})
390402
// should NOT include modelModifiers in dynamicPropNames because it's never
391403
// gonna change
392-
expect(args[4]).toBe(`["modelValue"]`)
404+
expect(args[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
393405
})
394406

395407
describe('errors', () => {

packages/compiler-core/__tests__/transforms/vOn.spec.ts

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@ import {
66
CompilerOptions,
77
ErrorCodes,
88
NodeTypes,
9-
CallExpression
9+
CallExpression,
10+
PlainElementCodegenNode
1011
} from '../../src'
1112
import { transformOn } from '../../src/transforms/vOn'
1213
import { transformElement } from '../../src/transforms/transformElement'
1314
import { transformExpression } from '../../src/transforms/transformExpression'
1415

15-
function parseWithVOn(
16-
template: string,
17-
options: CompilerOptions = {}
18-
): ElementNode {
16+
function parseWithVOn(template: string, options: CompilerOptions = {}) {
1917
const ast = parse(template)
2018
transform(ast, {
2119
nodeTransforms: [transformExpression, transformElement],
@@ -24,12 +22,15 @@ function parseWithVOn(
2422
},
2523
...options
2624
})
27-
return ast.children[0] as ElementNode
25+
return {
26+
root: ast,
27+
node: ast.children[0] as ElementNode
28+
}
2829
}
2930

3031
describe('compiler: transform v-on', () => {
3132
test('basic', () => {
32-
const node = parseWithVOn(`<div v-on:click="onClick"/>`)
33+
const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
3334
const props = (node.codegenNode as CallExpression)
3435
.arguments[1] as ObjectExpression
3536
expect(props.properties[0]).toMatchObject({
@@ -65,7 +66,7 @@ describe('compiler: transform v-on', () => {
6566
})
6667

6768
test('dynamic arg', () => {
68-
const node = parseWithVOn(`<div v-on:[event]="handler"/>`)
69+
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`)
6970
const props = (node.codegenNode as CallExpression)
7071
.arguments[1] as ObjectExpression
7172
expect(props.properties[0]).toMatchObject({
@@ -82,7 +83,7 @@ describe('compiler: transform v-on', () => {
8283
})
8384

8485
test('dynamic arg with prefixing', () => {
85-
const node = parseWithVOn(`<div v-on:[event]="handler"/>`, {
86+
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`, {
8687
prefixIdentifiers: true
8788
})
8889
const props = (node.codegenNode as CallExpression)
@@ -101,7 +102,7 @@ describe('compiler: transform v-on', () => {
101102
})
102103

103104
test('dynamic arg with complex exp prefixing', () => {
104-
const node = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
105+
const { node } = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
105106
prefixIdentifiers: true
106107
})
107108
const props = (node.codegenNode as CallExpression)
@@ -127,7 +128,7 @@ describe('compiler: transform v-on', () => {
127128
})
128129

129130
test('should wrap as function if expression is inline statement', () => {
130-
const node = parseWithVOn(`<div @click="i++"/>`)
131+
const { node } = parseWithVOn(`<div @click="i++"/>`)
131132
const props = (node.codegenNode as CallExpression)
132133
.arguments[1] as ObjectExpression
133134
expect(props.properties[0]).toMatchObject({
@@ -140,7 +141,7 @@ describe('compiler: transform v-on', () => {
140141
})
141142

142143
test('inline statement w/ prefixIdentifiers: true', () => {
143-
const node = parseWithVOn(`<div @click="foo($event)"/>`, {
144+
const { node } = parseWithVOn(`<div @click="foo($event)"/>`, {
144145
prefixIdentifiers: true
145146
})
146147
const props = (node.codegenNode as CallExpression)
@@ -163,7 +164,7 @@ describe('compiler: transform v-on', () => {
163164
})
164165

165166
test('should NOT wrap as function if expression is already function expression', () => {
166-
const node = parseWithVOn(`<div @click="$event => foo($event)"/>`)
167+
const { node } = parseWithVOn(`<div @click="$event => foo($event)"/>`)
167168
const props = (node.codegenNode as CallExpression)
168169
.arguments[1] as ObjectExpression
169170
expect(props.properties[0]).toMatchObject({
@@ -176,7 +177,7 @@ describe('compiler: transform v-on', () => {
176177
})
177178

178179
test('should NOT wrap as function if expression is complex member expression', () => {
179-
const node = parseWithVOn(`<div @click="a['b' + c]"/>`)
180+
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`)
180181
const props = (node.codegenNode as CallExpression)
181182
.arguments[1] as ObjectExpression
182183
expect(props.properties[0]).toMatchObject({
@@ -189,7 +190,7 @@ describe('compiler: transform v-on', () => {
189190
})
190191

191192
test('complex member expression w/ prefixIdentifiers: true', () => {
192-
const node = parseWithVOn(`<div @click="a['b' + c]"/>`, {
193+
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`, {
193194
prefixIdentifiers: true
194195
})
195196
const props = (node.codegenNode as CallExpression)
@@ -204,7 +205,7 @@ describe('compiler: transform v-on', () => {
204205
})
205206

206207
test('function expression w/ prefixIdentifiers: true', () => {
207-
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
208+
const { node } = parseWithVOn(`<div @click="e => foo(e)"/>`, {
208209
prefixIdentifiers: true
209210
})
210211
const props = (node.codegenNode as CallExpression)
@@ -249,5 +250,81 @@ describe('compiler: transform v-on', () => {
249250
expect(onError).not.toHaveBeenCalled()
250251
})
251252

252-
test.todo('.once modifier')
253+
describe('cacheHandler', () => {
254+
test('empty handler', () => {
255+
const { root, node } = parseWithVOn(`<div v-on:click.prevent />`, {
256+
prefixIdentifiers: true,
257+
cacheHandlers: true
258+
})
259+
expect(root.cached).toBe(1)
260+
const args = (node.codegenNode as PlainElementCodegenNode).arguments
261+
// should not treat cached handler as dynamicProp, so no flags
262+
expect(args.length).toBe(2)
263+
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
264+
type: NodeTypes.JS_CACHE_EXPRESSION,
265+
index: 1,
266+
value: {
267+
type: NodeTypes.SIMPLE_EXPRESSION,
268+
content: `() => {}`
269+
}
270+
})
271+
})
272+
273+
test('member expression handler', () => {
274+
const { root, node } = parseWithVOn(`<div v-on:click="foo" />`, {
275+
prefixIdentifiers: true,
276+
cacheHandlers: true
277+
})
278+
expect(root.cached).toBe(1)
279+
const args = (node.codegenNode as PlainElementCodegenNode).arguments
280+
// should not treat cached handler as dynamicProp, so no flags
281+
expect(args.length).toBe(2)
282+
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
283+
type: NodeTypes.JS_CACHE_EXPRESSION,
284+
index: 1,
285+
value: {
286+
type: NodeTypes.COMPOUND_EXPRESSION,
287+
children: [`$event => (`, { content: `_ctx.foo($event)` }, `)`]
288+
}
289+
})
290+
})
291+
292+
test('inline function expression handler', () => {
293+
const { root, node } = parseWithVOn(`<div v-on:click="() => foo()" />`, {
294+
prefixIdentifiers: true,
295+
cacheHandlers: true
296+
})
297+
expect(root.cached).toBe(1)
298+
const args = (node.codegenNode as PlainElementCodegenNode).arguments
299+
// should not treat cached handler as dynamicProp, so no flags
300+
expect(args.length).toBe(2)
301+
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
302+
type: NodeTypes.JS_CACHE_EXPRESSION,
303+
index: 1,
304+
value: {
305+
type: NodeTypes.COMPOUND_EXPRESSION,
306+
children: [`() => `, { content: `_ctx.foo` }, `()`]
307+
}
308+
})
309+
})
310+
311+
test('inline statement handler', () => {
312+
const { root, node } = parseWithVOn(`<div v-on:click="foo++" />`, {
313+
prefixIdentifiers: true,
314+
cacheHandlers: true
315+
})
316+
expect(root.cached).toBe(1)
317+
const args = (node.codegenNode as PlainElementCodegenNode).arguments
318+
// should not treat cached handler as dynamicProp, so no flags
319+
expect(args.length).toBe(2)
320+
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
321+
type: NodeTypes.JS_CACHE_EXPRESSION,
322+
index: 1,
323+
value: {
324+
type: NodeTypes.COMPOUND_EXPRESSION,
325+
children: [`$event => (`, { content: `_ctx.foo` }, `++`, `)`]
326+
}
327+
})
328+
})
329+
})
253330
})

0 commit comments

Comments
 (0)