feat(v-on): cache handlers

This commit is contained in:
Evan You
2019-10-18 21:51:34 -04:00
parent 39ea67a2d2
commit 58593c4714
19 changed files with 529 additions and 243 deletions

View File

@@ -13,7 +13,8 @@ import {
createCallExpression,
createConditionalExpression,
IfCodegenNode,
ForCodegenNode
ForCodegenNode,
createCacheExpression
} from '../src'
import {
CREATE_VNODE,
@@ -34,6 +35,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
components: [],
directives: [],
hoists: [],
cached: 0,
codegenNode: createSimpleExpression(`null`, false),
loc: locStub,
...options
@@ -135,6 +137,12 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot()
})
test('cached', () => {
const root = createRoot({ cached: 3 })
const { code } = generate(root)
expect(code).toMatch(`let _cached_1, _cached_2, _cached_3`)
})
test('prefixIdentifiers: true should inject _ctx statement', () => {
const { code } = generate(createRoot(), { prefixIdentifiers: true })
expect(code).toMatch(`const _ctx = this\n`)
@@ -359,4 +367,16 @@ describe('compiler: codegen', () => {
)
expect(code).toMatchSnapshot()
})
test('CacheExpression', () => {
const { code } = generate(
createRoot({
codegenNode: createCacheExpression(
1,
createSimpleExpression(`foo`, false)
)
})
)
expect(code).toMatch(`_cached_1 || (_cached_1 = foo)`)
})
})

View File

@@ -203,6 +203,24 @@ return function render() {
}"
`;
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist elements with cached handlers 1`] = `
"const _Vue = Vue
let _cached_1
return function render() {
with (this) {
const { createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
return (_openBlock(), _createBlock(\\"div\\", null, [
_createVNode(\\"div\\", {
onClick: _cached_1 || (_cached_1 = $event => (_ctx.foo($event)))
})
]))
}
}"
`;
exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (2) 1`] = `
"const _Vue = Vue

View File

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

View File

@@ -18,6 +18,7 @@ import { transformExpression } from '../../src/transforms/transformExpression'
import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind'
import { transformOn } from '../../src/transforms/vOn'
import { createObjectMatcher, genFlagText } from '../testUtils'
import { PatchFlags } from '@vue/shared'
@@ -25,7 +26,6 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
hoistStatic: true,
prefixIdentifiers: options.prefixIdentifiers,
nodeTransforms: [
transformIf,
transformFor,
@@ -33,8 +33,10 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
transformElement
],
directiveTransforms: {
on: transformOn,
bind: transformBind
}
},
...options
})
expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
@@ -656,5 +658,16 @@ describe('compiler: hoistStatic transform', () => {
expect(root.hoists.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
test('should NOT hoist elements with cached handlers', () => {
const { root } = transformWithHoist(`<div><div @click="foo"/></div>`, {
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
expect(root.hoists.length).toBe(0)
expect(generate(root).code).toMatchSnapshot()
})
})
})

View File

@@ -9,7 +9,8 @@ import {
ForNode,
PlainElementNode,
PlainElementCodegenNode,
ComponentNode
ComponentNode,
NodeTypes
} from '../../src'
import { ErrorCodes } from '../../src/errors'
import { transformModel } from '../../src/transforms/vModel'
@@ -338,25 +339,36 @@ describe('compiler: transform v-model', () => {
expect(generate(root, { mode: 'module' }).code).toMatchSnapshot()
})
test('should not mark update handler dynamic', () => {
test('should cache update handler w/ cacheHandlers: true', () => {
const root = parseWithVModel('<input v-model="foo" />', {
prefixIdentifiers: true
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
const codegen = (root.children[0] as PlainElementNode)
.codegenNode as PlainElementCodegenNode
// should not list cached prop in dynamicProps
expect(codegen.arguments[4]).toBe(`["modelValue"]`)
expect(
(codegen.arguments[1] as ObjectExpression).properties[1].value.type
).toBe(NodeTypes.JS_CACHE_EXPRESSION)
})
test('should mark update handler dynamic if it refers v-for scope variables', () => {
test('should not cache update handler if it refers v-for scope variables', () => {
const root = parseWithVModel(
'<input v-for="i in list" v-model="foo[i]" />',
{
prefixIdentifiers: true
prefixIdentifiers: true,
cacheHandlers: true
}
)
expect(root.cached).toBe(0)
const codegen = ((root.children[0] as ForNode)
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
expect(
(codegen.arguments[1] as ObjectExpression).properties[1].value.type
).not.toBe(NodeTypes.JS_CACHE_EXPRESSION)
})
test('should mark update handler dynamic if it refers slot scope variables', () => {
@@ -389,7 +401,7 @@ describe('compiler: transform v-model', () => {
})
// should NOT include modelModifiers in dynamicPropNames because it's never
// gonna change
expect(args[4]).toBe(`["modelValue"]`)
expect(args[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
})
describe('errors', () => {

View File

@@ -6,16 +6,14 @@ import {
CompilerOptions,
ErrorCodes,
NodeTypes,
CallExpression
CallExpression,
PlainElementCodegenNode
} from '../../src'
import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement'
import { transformExpression } from '../../src/transforms/transformExpression'
function parseWithVOn(
template: string,
options: CompilerOptions = {}
): ElementNode {
function parseWithVOn(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformExpression, transformElement],
@@ -24,12 +22,15 @@ function parseWithVOn(
},
...options
})
return ast.children[0] as ElementNode
return {
root: ast,
node: ast.children[0] as ElementNode
}
}
describe('compiler: transform v-on', () => {
test('basic', () => {
const node = parseWithVOn(`<div v-on:click="onClick"/>`)
const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
@@ -65,7 +66,7 @@ describe('compiler: transform v-on', () => {
})
test('dynamic arg', () => {
const node = parseWithVOn(`<div v-on:[event]="handler"/>`)
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
@@ -82,7 +83,7 @@ describe('compiler: transform v-on', () => {
})
test('dynamic arg with prefixing', () => {
const node = parseWithVOn(`<div v-on:[event]="handler"/>`, {
const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
@@ -101,7 +102,7 @@ describe('compiler: transform v-on', () => {
})
test('dynamic arg with complex exp prefixing', () => {
const node = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
const { node } = parseWithVOn(`<div v-on:[event(foo)]="handler"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
@@ -127,7 +128,7 @@ describe('compiler: transform v-on', () => {
})
test('should wrap as function if expression is inline statement', () => {
const node = parseWithVOn(`<div @click="i++"/>`)
const { node } = parseWithVOn(`<div @click="i++"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
@@ -140,7 +141,7 @@ describe('compiler: transform v-on', () => {
})
test('inline statement w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="foo($event)"/>`, {
const { node } = parseWithVOn(`<div @click="foo($event)"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
@@ -163,7 +164,7 @@ describe('compiler: transform v-on', () => {
})
test('should NOT wrap as function if expression is already function expression', () => {
const node = parseWithVOn(`<div @click="$event => foo($event)"/>`)
const { node } = parseWithVOn(`<div @click="$event => foo($event)"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
@@ -176,7 +177,7 @@ describe('compiler: transform v-on', () => {
})
test('should NOT wrap as function if expression is complex member expression', () => {
const node = parseWithVOn(`<div @click="a['b' + c]"/>`)
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`)
const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({
@@ -189,7 +190,7 @@ describe('compiler: transform v-on', () => {
})
test('complex member expression w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="a['b' + c]"/>`, {
const { node } = parseWithVOn(`<div @click="a['b' + c]"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
@@ -204,7 +205,7 @@ describe('compiler: transform v-on', () => {
})
test('function expression w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, {
const { node } = parseWithVOn(`<div @click="e => foo(e)"/>`, {
prefixIdentifiers: true
})
const props = (node.codegenNode as CallExpression)
@@ -249,5 +250,81 @@ describe('compiler: transform v-on', () => {
expect(onError).not.toHaveBeenCalled()
})
test.todo('.once modifier')
describe('cacheHandler', () => {
test('empty handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click.prevent />`, {
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `() => {}`
}
})
})
test('member expression handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click="foo" />`, {
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `_ctx.foo($event)` }, `)`]
}
})
})
test('inline function expression handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click="() => foo()" />`, {
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`() => `, { content: `_ctx.foo` }, `()`]
}
})
})
test('inline statement handler', () => {
const { root, node } = parseWithVOn(`<div v-on:click="foo++" />`, {
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
const args = (node.codegenNode as PlainElementCodegenNode).arguments
// should not treat cached handler as dynamicProp, so no flags
expect(args.length).toBe(2)
expect((args[1] as ObjectExpression).properties[0].value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`$event => (`, { content: `_ctx.foo` }, `++`, `)`]
}
})
})
})
})