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, createCallExpression,
createConditionalExpression, createConditionalExpression,
IfCodegenNode, IfCodegenNode,
ForCodegenNode ForCodegenNode,
createCacheExpression
} from '../src' } from '../src'
import { import {
CREATE_VNODE, CREATE_VNODE,
@ -34,6 +35,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
components: [], components: [],
directives: [], directives: [],
hoists: [], hoists: [],
cached: 0,
codegenNode: createSimpleExpression(`null`, false), codegenNode: createSimpleExpression(`null`, false),
loc: locStub, loc: locStub,
...options ...options
@ -135,6 +137,12 @@ describe('compiler: codegen', () => {
expect(code).toMatchSnapshot() 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', () => { test('prefixIdentifiers: true should inject _ctx statement', () => {
const { code } = generate(createRoot(), { prefixIdentifiers: true }) const { code } = generate(createRoot(), { prefixIdentifiers: true })
expect(code).toMatch(`const _ctx = this\n`) expect(code).toMatch(`const _ctx = this\n`)
@ -359,4 +367,16 @@ describe('compiler: codegen', () => {
) )
expect(code).toMatchSnapshot() 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`] = ` exports[`compiler: hoistStatic transform prefixIdentifiers should NOT hoist expressions that refer scope variables (2) 1`] = `
"const _Vue = Vue "const _Vue = Vue

View File

@ -8,7 +8,7 @@ export default function render() {
return (openBlock(), createBlock(\\"input\\", { return (openBlock(), createBlock(\\"input\\", {
modelValue: _ctx.model[_ctx.index], modelValue: _ctx.model[_ctx.index],
\\"onUpdate:modelValue\\": $event => (_ctx.model[_ctx.index] = $event) \\"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\\", { return (openBlock(), createBlock(\\"input\\", {
modelValue: _ctx.model, modelValue: _ctx.model,
\\"onUpdate:modelValue\\": $event => (_ctx.model = $event) \\"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 { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'
import { transformBind } from '../../src/transforms/vBind' import { transformBind } from '../../src/transforms/vBind'
import { transformOn } from '../../src/transforms/vOn'
import { createObjectMatcher, genFlagText } from '../testUtils' import { createObjectMatcher, genFlagText } from '../testUtils'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
@ -25,7 +26,6 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
hoistStatic: true, hoistStatic: true,
prefixIdentifiers: options.prefixIdentifiers,
nodeTransforms: [ nodeTransforms: [
transformIf, transformIf,
transformFor, transformFor,
@ -33,8 +33,10 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
transformElement transformElement
], ],
directiveTransforms: { directiveTransforms: {
on: transformOn,
bind: transformBind bind: transformBind
} },
...options
}) })
expect(ast.codegenNode).toMatchObject({ expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION, type: NodeTypes.JS_SEQUENCE_EXPRESSION,
@ -656,5 +658,16 @@ describe('compiler: hoistStatic transform', () => {
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() 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, ForNode,
PlainElementNode, PlainElementNode,
PlainElementCodegenNode, PlainElementCodegenNode,
ComponentNode ComponentNode,
NodeTypes
} from '../../src' } from '../../src'
import { ErrorCodes } from '../../src/errors' import { ErrorCodes } from '../../src/errors'
import { transformModel } from '../../src/transforms/vModel' import { transformModel } from '../../src/transforms/vModel'
@ -338,25 +339,36 @@ describe('compiler: transform v-model', () => {
expect(generate(root, { mode: 'module' }).code).toMatchSnapshot() 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" />', { 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) const codegen = (root.children[0] as PlainElementNode)
.codegenNode as PlainElementCodegenNode .codegenNode as PlainElementCodegenNode
// should not list cached prop in dynamicProps
expect(codegen.arguments[4]).toBe(`["modelValue"]`) 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( const root = parseWithVModel(
'<input v-for="i in list" v-model="foo[i]" />', '<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) const codegen = ((root.children[0] as ForNode)
.children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode .children[0] as PlainElementNode).codegenNode as PlainElementCodegenNode
expect(codegen.arguments[4]).toBe(`["modelValue", "onUpdate:modelValue"]`) 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', () => { 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 // should NOT include modelModifiers in dynamicPropNames because it's never
// gonna change // gonna change
expect(args[4]).toBe(`["modelValue"]`) expect(args[4]).toBe(`["modelValue", "onUpdate:modelValue"]`)
}) })
describe('errors', () => { describe('errors', () => {

View File

@ -6,16 +6,14 @@ import {
CompilerOptions, CompilerOptions,
ErrorCodes, ErrorCodes,
NodeTypes, NodeTypes,
CallExpression CallExpression,
PlainElementCodegenNode
} from '../../src' } from '../../src'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'
import { transformExpression } from '../../src/transforms/transformExpression' import { transformExpression } from '../../src/transforms/transformExpression'
function parseWithVOn( function parseWithVOn(template: string, options: CompilerOptions = {}) {
template: string,
options: CompilerOptions = {}
): ElementNode {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [transformExpression, transformElement], nodeTransforms: [transformExpression, transformElement],
@ -24,12 +22,15 @@ function parseWithVOn(
}, },
...options ...options
}) })
return ast.children[0] as ElementNode return {
root: ast,
node: ast.children[0] as ElementNode
}
} }
describe('compiler: transform v-on', () => { describe('compiler: transform v-on', () => {
test('basic', () => { test('basic', () => {
const node = parseWithVOn(`<div v-on:click="onClick"/>`) const { node } = parseWithVOn(`<div v-on:click="onClick"/>`)
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression .arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
@ -65,7 +66,7 @@ describe('compiler: transform v-on', () => {
}) })
test('dynamic arg', () => { 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) const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression .arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
@ -82,7 +83,7 @@ describe('compiler: transform v-on', () => {
}) })
test('dynamic arg with prefixing', () => { test('dynamic arg with prefixing', () => {
const node = parseWithVOn(`<div v-on:[event]="handler"/>`, { const { node } = parseWithVOn(`<div v-on:[event]="handler"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as CallExpression)
@ -101,7 +102,7 @@ describe('compiler: transform v-on', () => {
}) })
test('dynamic arg with complex exp prefixing', () => { 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 prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) 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', () => { 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) const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression .arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
@ -140,7 +141,7 @@ describe('compiler: transform v-on', () => {
}) })
test('inline statement w/ prefixIdentifiers: true', () => { test('inline statement w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="foo($event)"/>`, { const { node } = parseWithVOn(`<div @click="foo($event)"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) 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', () => { 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) const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression .arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ 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', () => { 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) const props = (node.codegenNode as CallExpression)
.arguments[1] as ObjectExpression .arguments[1] as ObjectExpression
expect(props.properties[0]).toMatchObject({ expect(props.properties[0]).toMatchObject({
@ -189,7 +190,7 @@ describe('compiler: transform v-on', () => {
}) })
test('complex member expression w/ prefixIdentifiers: true', () => { 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 prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as CallExpression)
@ -204,7 +205,7 @@ describe('compiler: transform v-on', () => {
}) })
test('function expression w/ prefixIdentifiers: true', () => { test('function expression w/ prefixIdentifiers: true', () => {
const node = parseWithVOn(`<div @click="e => foo(e)"/>`, { const { node } = parseWithVOn(`<div @click="e => foo(e)"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const props = (node.codegenNode as CallExpression) const props = (node.codegenNode as CallExpression)
@ -249,5 +250,81 @@ describe('compiler: transform v-on', () => {
expect(onError).not.toHaveBeenCalled() 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` }, `++`, `)`]
}
})
})
})
}) })

View File

@ -42,7 +42,8 @@ export const enum NodeTypes {
JS_ARRAY_EXPRESSION, JS_ARRAY_EXPRESSION,
JS_FUNCTION_EXPRESSION, JS_FUNCTION_EXPRESSION,
JS_SEQUENCE_EXPRESSION, JS_SEQUENCE_EXPRESSION,
JS_CONDITIONAL_EXPRESSION JS_CONDITIONAL_EXPRESSION,
JS_CACHE_EXPRESSION
} }
export const enum ElementTypes { export const enum ElementTypes {
@ -93,6 +94,7 @@ export interface RootNode extends Node {
components: string[] components: string[]
directives: string[] directives: string[]
hoists: JSChildNode[] hoists: JSChildNode[]
cached: number
codegenNode: TemplateChildNode | JSChildNode | undefined codegenNode: TemplateChildNode | JSChildNode | undefined
} }
@ -236,6 +238,7 @@ export type JSChildNode =
| FunctionExpression | FunctionExpression
| ConditionalExpression | ConditionalExpression
| SequenceExpression | SequenceExpression
| CacheExpression
export interface CallExpression extends Node { export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION type: NodeTypes.JS_CALL_EXPRESSION
@ -283,6 +286,12 @@ export interface ConditionalExpression extends Node {
alternate: JSChildNode alternate: JSChildNode
} }
export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION
index: number
value: JSChildNode
}
// Codegen Node Types ---------------------------------------------------------- // Codegen Node Types ----------------------------------------------------------
// createVNode(...) // createVNode(...)
@ -605,3 +614,15 @@ export function createConditionalExpression(
loc: locStub loc: locStub
} }
} }
export function createCacheExpression(
index: number,
value: JSChildNode
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
loc: locStub
}
}

View File

@ -16,7 +16,8 @@ import {
SimpleExpressionNode, SimpleExpressionNode,
FunctionExpression, FunctionExpression,
SequenceExpression, SequenceExpression,
ConditionalExpression ConditionalExpression,
CacheExpression
} from './ast' } from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map' import { SourceMapGenerator, RawSourceMap } from 'source-map'
import { import {
@ -218,6 +219,7 @@ export function generate(
} }
} }
genHoists(ast.hoists, context) genHoists(ast.hoists, context)
genCached(ast.cached, context)
newline() newline()
push(`return `) push(`return `)
} else { } else {
@ -226,6 +228,7 @@ export function generate(
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`) push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
} }
genHoists(ast.hoists, context) genHoists(ast.hoists, context)
genCached(ast.cached, context)
newline() newline()
push(`export default `) push(`export default `)
} }
@ -315,6 +318,18 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
}) })
} }
function genCached(cached: number, context: CodegenContext) {
if (cached > 0) {
context.newline()
context.push(`let `)
for (let i = 0; i < cached; i++) {
context.push(`_cached_${i + 1}`)
if (i !== cached - 1) context.push(`, `)
}
context.newline()
}
}
function isText(n: string | CodegenNode) { function isText(n: string | CodegenNode) {
return ( return (
isString(n) || isString(n) ||
@ -419,6 +434,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_CONDITIONAL_EXPRESSION: case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context) genConditionalExpression(node, context)
break break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
/* istanbul ignore next */ /* istanbul ignore next */
default: default:
if (__DEV__) { if (__DEV__) {
@ -612,3 +630,9 @@ function genSequenceExpression(
genNodeList(node.expressions, context) genNodeList(node.expressions, context)
context.push(`)`) context.push(`)`)
} }
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
context.push(`_cached_${node.index} || (_cached_${node.index} = `)
genNode(node.value, context)
context.push(`)`)
}

View File

@ -13,7 +13,9 @@ import {
ElementTypes, ElementTypes,
ElementCodegenNode, ElementCodegenNode,
ComponentCodegenNode, ComponentCodegenNode,
createCallExpression createCallExpression,
CacheExpression,
createCacheExpression
} from './ast' } from './ast'
import { isString, isArray } from '@vue/shared' import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors' import { CompilerError, defaultOnError } from './errors'
@ -45,8 +47,13 @@ export type NodeTransform = (
export type DirectiveTransform = ( export type DirectiveTransform = (
dir: DirectiveNode, dir: DirectiveNode,
node: ElementNode, node: ElementNode,
context: TransformContext context: TransformContext,
) => { // a platform specific compiler can import the base transform and augment
// it by passing in this optional argument.
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
) => DirectiveTransformResult
export interface DirectiveTransformResult {
props: Property[] props: Property[]
needRuntime: boolean | symbol needRuntime: boolean | symbol
} }
@ -64,6 +71,7 @@ export interface TransformOptions {
directiveTransforms?: { [name: string]: DirectiveTransform } directiveTransforms?: { [name: string]: DirectiveTransform }
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
hoistStatic?: boolean hoistStatic?: boolean
cacheHandlers?: boolean
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
@ -73,6 +81,7 @@ export interface TransformContext extends Required<TransformOptions> {
components: Set<string> components: Set<string>
directives: Set<string> directives: Set<string>
hoists: JSChildNode[] hoists: JSChildNode[]
cached: number
identifiers: { [name: string]: number | undefined } identifiers: { [name: string]: number | undefined }
scopes: { scopes: {
vFor: number vFor: number
@ -91,6 +100,7 @@ export interface TransformContext extends Required<TransformOptions> {
addIdentifiers(exp: ExpressionNode | string): void addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: JSChildNode): SimpleExpressionNode hoist(exp: JSChildNode): SimpleExpressionNode
cache<T extends JSChildNode>(exp: T): CacheExpression | T
} }
function createTransformContext( function createTransformContext(
@ -98,6 +108,7 @@ function createTransformContext(
{ {
prefixIdentifiers = false, prefixIdentifiers = false,
hoistStatic = false, hoistStatic = false,
cacheHandlers = false,
nodeTransforms = [], nodeTransforms = [],
directiveTransforms = {}, directiveTransforms = {},
onError = defaultOnError onError = defaultOnError
@ -109,6 +120,7 @@ function createTransformContext(
components: new Set(), components: new Set(),
directives: new Set(), directives: new Set(),
hoists: [], hoists: [],
cached: 0,
identifiers: {}, identifiers: {},
scopes: { scopes: {
vFor: 0, vFor: 0,
@ -118,6 +130,7 @@ function createTransformContext(
}, },
prefixIdentifiers, prefixIdentifiers,
hoistStatic, hoistStatic,
cacheHandlers,
nodeTransforms, nodeTransforms,
directiveTransforms, directiveTransforms,
onError, onError,
@ -204,6 +217,14 @@ function createTransformContext(
false, false,
exp.loc exp.loc
) )
},
cache(exp) {
if (cacheHandlers) {
context.cached++
return createCacheExpression(context.cached, exp)
} else {
return exp
}
} }
} }
@ -273,6 +294,7 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
root.components = [...context.components] root.components = [...context.components]
root.directives = [...context.directives] root.directives = [...context.directives]
root.hoists = context.hoists root.hoists = context.hoists
root.cached = context.cached
} }
export function traverseChildren( export function traverseChildren(

View File

@ -8,17 +8,14 @@ import {
PlainElementNode, PlainElementNode,
ComponentNode, ComponentNode,
TemplateNode, TemplateNode,
ElementNode ElementNode,
PlainElementCodegenNode
} from '../ast' } from '../ast'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'
import { WITH_DIRECTIVES } from '../runtimeHelpers' import { WITH_DIRECTIVES } from '../runtimeHelpers'
import { PatchFlags, isString, isSymbol } from '@vue/shared' import { PatchFlags, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet, findProp } from '../utils' import { isSlotOutlet, findProp } from '../utils'
function hasDynamicKeyOrRef(node: ElementNode) {
return findProp(node, 'key', true) || findProp(node, 'ref', true)
}
export function hoistStatic(root: RootNode, context: TransformContext) { export function hoistStatic(root: RootNode, context: TransformContext) {
walk( walk(
root.children, root.children,
@ -53,10 +50,11 @@ function walk(
child.type === NodeTypes.ELEMENT && child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT child.tagType === ElementTypes.ELEMENT
) { ) {
const hasBailoutProp = hasDynamicKeyOrRef(child) || hasCachedProps(child)
if ( if (
!doNotHoistNode && !doNotHoistNode &&
isStaticNode(child, resultCache) && !hasBailoutProp &&
!hasDynamicKeyOrRef(child) isStaticNode(child, resultCache)
) { ) {
// whole tree is static // whole tree is static
child.codegenNode = context.hoist(child.codegenNode!) child.codegenNode = context.hoist(child.codegenNode!)
@ -69,15 +67,11 @@ function walk(
(!flag || (!flag ||
flag === PatchFlags.NEED_PATCH || flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) && flag === PatchFlags.TEXT) &&
!hasDynamicKeyOrRef(child) !hasBailoutProp
) { ) {
let codegenNode = child.codegenNode as ElementCodegenNode const props = getNodeProps(child)
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
const props = codegenNode.arguments[1]
if (props && props !== `null`) { if (props && props !== `null`) {
codegenNode.arguments[1] = context.hoist(props) getVNodeCall(child).arguments[1] = context.hoist(props)
} }
} }
} }
@ -97,15 +91,6 @@ function walk(
} }
} }
function getPatchFlag(node: PlainElementNode): number | undefined {
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
const flag = codegenNode.arguments[3]
return flag ? parseInt(flag, 10) : undefined
}
export function isStaticNode( export function isStaticNode(
node: TemplateChildNode | SimpleExpressionNode, node: TemplateChildNode | SimpleExpressionNode,
resultCache: Map<TemplateChildNode, boolean> = new Map() resultCache: Map<TemplateChildNode, boolean> = new Map()
@ -157,3 +142,51 @@ export function isStaticNode(
return false return false
} }
} }
function hasDynamicKeyOrRef(node: ElementNode): boolean {
return !!(findProp(node, 'key', true) || findProp(node, 'ref', true))
}
function hasCachedProps(node: PlainElementNode): boolean {
if (__BROWSER__) {
return false
}
const props = getNodeProps(node)
if (
props &&
props !== 'null' &&
props.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
const { properties } = props
for (let i = 0; i < properties.length; i++) {
if (properties[i].value.type === NodeTypes.JS_CACHE_EXPRESSION) {
return true
}
}
}
return false
}
function getVNodeCall(node: PlainElementNode) {
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
return codegenNode
}
function getVNodeArgAt(
node: PlainElementNode,
index: number
): PlainElementCodegenNode['arguments'][number] {
return getVNodeCall(node).arguments[index]
}
function getPatchFlag(node: PlainElementNode): number | undefined {
const flag = getVNodeArgAt(node, 3) as string
return flag ? parseInt(flag, 10) : undefined
}
function getNodeProps(node: PlainElementNode) {
return getVNodeArgAt(node, 1) as PlainElementCodegenNode['arguments'][1]
}

View File

@ -222,9 +222,10 @@ export function buildProps(
const analyzePatchFlag = ({ key, value }: Property) => { const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
if ( if (
(value.type === NodeTypes.SIMPLE_EXPRESSION || value.type === NodeTypes.JS_CACHE_EXPRESSION ||
((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) && value.type === NodeTypes.COMPOUND_EXPRESSION) &&
isStaticNode(value) isStaticNode(value))
) { ) {
return return
} }

View File

@ -1,17 +1,14 @@
import { DirectiveTransform, TransformContext } from '../transform' import { DirectiveTransform } from '../transform'
import { import {
createSimpleExpression, createSimpleExpression,
createObjectProperty, createObjectProperty,
createCompoundExpression, createCompoundExpression,
NodeTypes, NodeTypes,
Property, Property,
CompoundExpressionNode,
createInterpolation,
ElementTypes ElementTypes
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { isMemberExpression, isSimpleIdentifier } from '../utils' import { isMemberExpression, isSimpleIdentifier, hasScopeRef } from '../utils'
import { isObject } from '@vue/shared'
export const transformModel: DirectiveTransform = (dir, node, context) => { export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir const { exp, arg } = dir
@ -54,16 +51,6 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
]) ])
: createSimpleExpression('onUpdate:modelValue', true) : createSimpleExpression('onUpdate:modelValue', true)
let assignmentChildren =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children
// For a member expression used in assignment, it only needs to be updated
// if the expression involves scope variables. Otherwise we can mark the
// expression as constant to avoid it being included in `dynamicPropNames`
// of the element. This optimization relies on `prefixIdentifiers: true`.
if (!__BROWSER__ && context.prefixIdentifiers) {
assignmentChildren = assignmentChildren.map(c => toConstant(c, context))
}
const props = [ const props = [
// modelValue: foo // modelValue: foo
createObjectProperty(propName, dir.exp!), createObjectProperty(propName, dir.exp!),
@ -72,12 +59,21 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
eventName, eventName,
createCompoundExpression([ createCompoundExpression([
`$event => (`, `$event => (`,
...assignmentChildren, ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
` = $event)` ` = $event)`
]) ])
) )
] ]
// cache v-model handler if applicable (when it doesn't refer any scope vars)
if (
!__BROWSER__ &&
context.prefixIdentifiers &&
!hasScopeRef(exp, context.identifiers)
) {
props[1].value = context.cache(props[1].value)
}
// modelModifiers: { foo: true, "bar-baz": true } // modelModifiers: { foo: true, "bar-baz": true }
if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) { if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) {
const modifiers = dir.modifiers const modifiers = dir.modifiers
@ -94,30 +90,6 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
return createTransformProps(props) return createTransformProps(props)
} }
function toConstant(
exp: CompoundExpressionNode | CompoundExpressionNode['children'][0],
context: TransformContext
): any {
if (!isObject(exp) || exp.type === NodeTypes.TEXT) {
return exp
}
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
if (exp.isStatic || context.identifiers[exp.content]) {
return exp
}
return {
...exp,
isConstant: true
}
} else if (exp.type === NodeTypes.COMPOUND_EXPRESSION) {
return createCompoundExpression(
exp.children.map(c => toConstant(c, context))
)
} else if (exp.type === NodeTypes.INTERPOLATION) {
return createInterpolation(toConstant(exp.content, context), exp.loc)
}
}
function createTransformProps(props: Property[] = []) { function createTransformProps(props: Property[] = []) {
return { props, needRuntime: false } return { props, needRuntime: false }
} }

View File

@ -1,4 +1,4 @@
import { DirectiveTransform } from '../transform' import { DirectiveTransform, DirectiveTransformResult } from '../transform'
import { import {
DirectiveNode, DirectiveNode,
createObjectProperty, createObjectProperty,
@ -11,7 +11,7 @@ import {
import { capitalize } from '@vue/shared' import { capitalize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { isMemberExpression } from '../utils' import { isMemberExpression, hasScopeRef } from '../utils'
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/ const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
@ -28,7 +28,8 @@ export interface VOnDirectiveNode extends DirectiveNode {
export const transformOn: DirectiveTransform = ( export const transformOn: DirectiveTransform = (
dir: VOnDirectiveNode, dir: VOnDirectiveNode,
node, node,
context context,
augmentor
) => { ) => {
const { loc, modifiers, arg } = dir const { loc, modifiers, arg } = dir
if (!dir.exp && !modifiers.length) { if (!dir.exp && !modifiers.length) {
@ -51,22 +52,37 @@ export const transformOn: DirectiveTransform = (
eventName.children.unshift(`"on" + (`) eventName.children.unshift(`"on" + (`)
eventName.children.push(`)`) eventName.children.push(`)`)
} }
// TODO .once modifier handling since it is platform agnostic
// other modifiers are handled in compiler-dom
// handler processing // handler processing
let exp: ExpressionNode | undefined = dir.exp let exp: ExpressionNode | undefined = dir.exp
let isCacheable: boolean = !exp
if (exp) { if (exp) {
const isInlineStatement = !( const isMemberExp = isMemberExpression(exp.content)
isMemberExpression(exp.content) || fnExpRE.test(exp.content) const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
)
// process the expression since it's been skipped // process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) { if (!__BROWSER__ && context.prefixIdentifiers) {
context.addIdentifiers(`$event`) context.addIdentifiers(`$event`)
exp = processExpression(exp, context) exp = processExpression(exp, context)
context.removeIdentifiers(`$event`) context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference
// to scope variables.
isCacheable =
context.cacheHandlers && !hasScopeRef(exp, context.identifiers)
// If the expression is optimizable and is a member expression pointing
// to a function, turn it into invocation (and wrap in an arrow function
// below) so that it always accesses the latest value when called - thus
// avoiding the need to be patched.
if (isCacheable && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content += `($event)`
} else {
exp.children.push(`($event)`)
} }
if (isInlineStatement) { }
}
if (isInlineStatement || (isCacheable && isMemberExp)) {
// wrap inline statement in a function expression // wrap inline statement in a function expression
exp = createCompoundExpression([ exp = createCompoundExpression([
`$event => (`, `$event => (`,
@ -76,7 +92,7 @@ export const transformOn: DirectiveTransform = (
} }
} }
return { let ret: DirectiveTransformResult = {
props: [ props: [
createObjectProperty( createObjectProperty(
eventName, eventName,
@ -85,4 +101,18 @@ export const transformOn: DirectiveTransform = (
], ],
needRuntime: false needRuntime: false
} }
// apply extended compiler augmentor
if (augmentor) {
ret = augmentor(ret)
}
if (isCacheable) {
// cache handlers so that it's always the same handler being passed down.
// this avoids unnecessary re-renders when users use inline hanlders on
// components.
ret.props[0].value = context.cache(ret.props[0].value)
}
return ret
} }

View File

@ -19,21 +19,13 @@ import {
FunctionExpression, FunctionExpression,
CallExpression, CallExpression,
createCallExpression, createCallExpression,
createArrayExpression, createArrayExpression
IfBranchNode
} from '../ast' } from '../ast'
import { TransformContext, NodeTransform } from '../transform' import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils'
findDir,
isTemplateNode,
assert,
isVSlot,
isSimpleIdentifier
} from '../utils'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers' import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor' import { parseForExpression, createForLoopParams } from './vFor'
import { isObject } from '@vue/shared'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode => const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
@ -337,49 +329,3 @@ function buildDynamicSlot(
createObjectProperty(`fn`, fn) createObjectProperty(`fn`, fn)
]) ])
} }
function hasScopeRef(
node: TemplateChildNode | IfBranchNode | SimpleExpressionNode | undefined,
ids: TransformContext['identifiers']
): boolean {
if (!node || Object.keys(ids).length === 0) {
return false
}
switch (node.type) {
case NodeTypes.ELEMENT:
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
(hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
) {
return true
}
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.FOR:
if (hasScopeRef(node.source, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.IF:
return node.branches.some(b => hasScopeRef(b, ids))
case NodeTypes.IF_BRANCH:
if (hasScopeRef(node.condition, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.SIMPLE_EXPRESSION:
return (
!node.isStatic &&
isSimpleIdentifier(node.content) &&
!!ids[node.content]
)
case NodeTypes.COMPOUND_EXPRESSION:
return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
case NodeTypes.INTERPOLATION:
return hasScopeRef(node.content, ids)
default:
return false
}
}

View File

@ -21,13 +21,14 @@ import {
ElementCodegenNode, ElementCodegenNode,
SlotOutletCodegenNode, SlotOutletCodegenNode,
ComponentCodegenNode, ComponentCodegenNode,
ExpressionNode ExpressionNode,
IfBranchNode
} from './ast' } from './ast'
import { parse } from 'acorn' import { parse } from 'acorn'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { TransformContext } from './transform' import { TransformContext } from './transform'
import { OPEN_BLOCK, MERGE_PROPS, RENDER_SLOT } from './runtimeHelpers' import { OPEN_BLOCK, MERGE_PROPS, RENDER_SLOT } from './runtimeHelpers'
import { isString, isFunction } from '@vue/shared' import { isString, isFunction, isObject } from '@vue/shared'
// cache node requires // cache node requires
// lazy require dependencies so that they don't end up in rollup's dep graph // lazy require dependencies so that they don't end up in rollup's dep graph
@ -250,3 +251,51 @@ export function toValidAssetId(
export function isEmptyExpression(node: ExpressionNode) { export function isEmptyExpression(node: ExpressionNode) {
return node.type === NodeTypes.SIMPLE_EXPRESSION && !node.content.trim() return node.type === NodeTypes.SIMPLE_EXPRESSION && !node.content.trim()
} }
// Check if a node contains expressions that reference current context scope ids
export function hasScopeRef(
node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined,
ids: TransformContext['identifiers']
): boolean {
if (!node || Object.keys(ids).length === 0) {
return false
}
switch (node.type) {
case NodeTypes.ELEMENT:
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
(hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
) {
return true
}
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.FOR:
if (hasScopeRef(node.source, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.IF:
return node.branches.some(b => hasScopeRef(b, ids))
case NodeTypes.IF_BRANCH:
if (hasScopeRef(node.condition, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.SIMPLE_EXPRESSION:
return (
!node.isStatic &&
isSimpleIdentifier(node.content) &&
!!ids[node.content]
)
case NodeTypes.COMPOUND_EXPRESSION:
return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
case NodeTypes.INTERPOLATION:
return hasScopeRef(node.content, ids)
default:
// TextNode or CommentNode
return false
}
}

View File

@ -5,8 +5,7 @@ import {
ElementNode, ElementNode,
ObjectExpression, ObjectExpression,
CallExpression, CallExpression,
NodeTypes, NodeTypes
Property
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { transformOn } from '../../src/transforms/vOn' import { transformOn } from '../../src/transforms/vOn'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers' import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers'
@ -14,10 +13,7 @@ import { transformElement } from '../../../compiler-core/src/transforms/transfor
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression' import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils' import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
function parseVOnProperties( function parseWithVOn(template: string, options: CompilerOptions = {}) {
template: string,
options: CompilerOptions = {}
): Property[] {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [transformExpression, transformElement], nodeTransforms: [transformExpression, transformElement],
@ -26,13 +22,18 @@ function parseVOnProperties(
}, },
...options ...options
}) })
return (((ast.children[0] as ElementNode).codegenNode as CallExpression) return {
root: ast,
props: (((ast.children[0] as ElementNode).codegenNode as CallExpression)
.arguments[1] as ObjectExpression).properties .arguments[1] as ObjectExpression).properties
}
} }
describe('compiler-dom: transform v-on', () => { describe('compiler-dom: transform v-on', () => {
it('should support multiple modifiers w/ prefixIdentifiers: true', () => { it('should support multiple modifiers w/ prefixIdentifiers: true', () => {
const [prop] = parseVOnProperties(`<div @click.stop.prevent="test"/>`, { const {
props: [prop]
} = parseWithVOn(`<div @click.stop.prevent="test"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
expect(prop).toMatchObject({ expect(prop).toMatchObject({
@ -45,10 +46,11 @@ describe('compiler-dom: transform v-on', () => {
}) })
it('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => { it('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => {
const [prop] = parseVOnProperties( const {
`<div @click.stop.capture.passive="test"/>`, props: [prop]
{ prefixIdentifiers: true } } = parseWithVOn(`<div @click.stop.capture.passive="test"/>`, {
) prefixIdentifiers: true
})
expect(prop).toMatchObject({ expect(prop).toMatchObject({
type: NodeTypes.JS_PROPERTY, type: NodeTypes.JS_PROPERTY,
value: createObjectMatcher({ value: createObjectMatcher({
@ -59,17 +61,17 @@ describe('compiler-dom: transform v-on', () => {
options: createObjectMatcher({ options: createObjectMatcher({
capture: { content: 'true', isStatic: false }, capture: { content: 'true', isStatic: false },
passive: { content: 'true', isStatic: false } passive: { content: 'true', isStatic: false }
}), })
persistent: { content: 'true', isStatic: false }
}) })
}) })
}) })
it('should wrap keys guard for keyboard events or dynamic events', () => { it('should wrap keys guard for keyboard events or dynamic events', () => {
const [prop] = parseVOnProperties( const {
`<div @keyDown.stop.capture.ctrl.a="test"/>`, props: [prop]
{ prefixIdentifiers: true } } = parseWithVOn(`<div @keyDown.stop.capture.ctrl.a="test"/>`, {
) prefixIdentifiers: true
})
expect(prop).toMatchObject({ expect(prop).toMatchObject({
type: NodeTypes.JS_PROPERTY, type: NodeTypes.JS_PROPERTY,
value: createObjectMatcher({ value: createObjectMatcher({
@ -85,14 +87,15 @@ describe('compiler-dom: transform v-on', () => {
}, },
options: createObjectMatcher({ options: createObjectMatcher({
capture: { content: 'true', isStatic: false } capture: { content: 'true', isStatic: false }
}), })
persistent: { content: 'true', isStatic: false }
}) })
}) })
}) })
it('should not wrap keys guard if no key modifier is present', () => { it('should not wrap keys guard if no key modifier is present', () => {
const [prop] = parseVOnProperties(`<div @keyup.exact="test"/>`, { const {
props: [prop]
} = parseWithVOn(`<div @keyup.exact="test"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
expect(prop).toMatchObject({ expect(prop).toMatchObject({
@ -105,7 +108,9 @@ describe('compiler-dom: transform v-on', () => {
}) })
it('should not wrap normal guard if there is only keys guard', () => { it('should not wrap normal guard if there is only keys guard', () => {
const [prop] = parseVOnProperties(`<div @keyup.enter="test"/>`, { const {
props: [prop]
} = parseWithVOn(`<div @keyup.enter="test"/>`, {
prefixIdentifiers: true prefixIdentifiers: true
}) })
expect(prop).toMatchObject({ expect(prop).toMatchObject({
@ -116,4 +121,37 @@ describe('compiler-dom: transform v-on', () => {
} }
}) })
}) })
test('cache handler w/ modifiers', () => {
const {
root,
props: [prop]
} = parseWithVOn(`<div @keyup.enter.capture="foo" />`, {
prefixIdentifiers: true,
cacheHandlers: true
})
expect(root.cached).toBe(1)
// should not treat cached handler as dynamicProp, so no flags
expect((root as any).children[0].codegenNode.arguments.length).toBe(2)
expect(prop.value).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1,
value: {
type: NodeTypes.JS_OBJECT_EXPRESSION,
properties: [
{
key: { content: 'handler' },
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: V_ON_WITH_KEYS
}
},
{
key: { content: 'options' },
value: { type: NodeTypes.JS_OBJECT_EXPRESSION }
}
]
}
})
})
}) })

View File

@ -25,8 +25,8 @@ const isKeyboardEvent = /*#__PURE__*/ makeMap(
) )
export const transformOn: DirectiveTransform = (dir, node, context) => { export const transformOn: DirectiveTransform = (dir, node, context) => {
return baseTransform(dir, node, context, baseResult => {
const { modifiers } = dir const { modifiers } = dir
const baseResult = baseTransform(dir, node, context)
if (!modifiers.length) return baseResult if (!modifiers.length) return baseResult
let { key, value: handlerExp } = baseResult.props[0] let { key, value: handlerExp } = baseResult.props[0]
@ -72,9 +72,7 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
) )
) )
) )
), )
// so the runtime knows the options never change
createObjectProperty('persistent', createSimpleExpression('true', false))
]) ])
} }
@ -82,4 +80,5 @@ export const transformOn: DirectiveTransform = (dir, node, context) => {
props: [createObjectProperty(key, handlerExp)], props: [createObjectProperty(key, handlerExp)],
needRuntime: false needRuntime: false
} }
})
} }

View File

@ -17,7 +17,6 @@ type EventValue = (Function | Function[]) & {
type EventValueWithOptions = { type EventValueWithOptions = {
handler: EventValue handler: EventValue
options: AddEventListenerOptions options: AddEventListenerOptions
persistent?: boolean
invoker?: Invoker | null invoker?: Invoker | null
} }
@ -77,10 +76,8 @@ export function patchEvent(
const invoker = prevValue && prevValue.invoker const invoker = prevValue && prevValue.invoker
const value = const value =
nextValue && 'handler' in nextValue ? nextValue.handler : nextValue nextValue && 'handler' in nextValue ? nextValue.handler : nextValue
const persistent =
nextValue && 'persistent' in nextValue && nextValue.persistent
if (!persistent && (prevOptions || nextOptions)) { if (prevOptions || nextOptions) {
const prev = prevOptions || EMPTY_OBJ const prev = prevOptions || EMPTY_OBJ
const next = nextOptions || EMPTY_OBJ const next = nextOptions || EMPTY_OBJ
if ( if (

View File

@ -4,7 +4,8 @@ import { CompilerOptions } from '@vue/compiler-dom'
export const compilerOptions: CompilerOptions = reactive({ export const compilerOptions: CompilerOptions = reactive({
mode: 'module', mode: 'module',
prefixIdentifiers: false, prefixIdentifiers: false,
hoistStatic: false hoistStatic: false,
cacheHandlers: false
}) })
const App = { const App = {
@ -70,7 +71,20 @@ const App = {
compilerOptions.hoistStatic = (<HTMLInputElement>e.target).checked compilerOptions.hoistStatic = (<HTMLInputElement>e.target).checked
} }
}), }),
h('label', { for: 'hoist' }, 'hoistStatic') h('label', { for: 'hoist' }, 'hoistStatic'),
// toggle cacheHandlers
h('input', {
type: 'checkbox',
id: 'cache',
checked:
compilerOptions.cacheHandlers && compilerOptions.prefixIdentifiers,
disabled: !compilerOptions.prefixIdentifiers,
onChange(e: Event) {
compilerOptions.cacheHandlers = (<HTMLInputElement>e.target).checked
}
}),
h('label', { for: 'cache' }, 'cacheHandlers')
]) ])
] ]
} }