createCallExpression,
createConditionalExpression,
IfCodegenNode,
- ForCodegenNode
+ ForCodegenNode,
+ createCacheExpression
} from '../src'
import {
CREATE_VNODE,
components: [],
directives: [],
hoists: [],
+ cached: 0,
codegenNode: createSimpleExpression(`null`, false),
loc: locStub,
...options
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`)
)
expect(code).toMatchSnapshot()
})
+
+ test('CacheExpression', () => {
+ const { code } = generate(
+ createRoot({
+ codegenNode: createCacheExpression(
+ 1,
+ createSimpleExpression(`foo`, false)
+ )
+ })
+ )
+ expect(code).toMatch(`_cached_1 || (_cached_1 = foo)`)
+ })
})
}"
`;
+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
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\\"]))
}"
`;
return (openBlock(), createBlock(\\"input\\", {
modelValue: _ctx.model,
\\"onUpdate:modelValue\\": $event => (_ctx.model = $event)
- }, null, 8 /* PROPS */, [\\"modelValue\\"]))
+ }, null, 8 /* PROPS */, [\\"modelValue\\", \\"onUpdate:modelValue\\"]))
}"
`;
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'
const ast = parse(template)
transform(ast, {
hoistStatic: true,
- prefixIdentifiers: options.prefixIdentifiers,
nodeTransforms: [
transformIf,
transformFor,
transformElement
],
directiveTransforms: {
+ on: transformOn,
bind: transformBind
- }
+ },
+ ...options
})
expect(ast.codegenNode).toMatchObject({
type: NodeTypes.JS_SEQUENCE_EXPRESSION,
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()
+ })
})
})
ForNode,
PlainElementNode,
PlainElementCodegenNode,
- ComponentNode
+ ComponentNode,
+ NodeTypes
} from '../../src'
import { ErrorCodes } from '../../src/errors'
import { transformModel } from '../../src/transforms/vModel'
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', () => {
})
// 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', () => {
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],
},
...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({
})
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({
})
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)
})
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)
})
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({
})
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)
})
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({
})
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({
})
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)
})
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)
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` }, `++`, `)`]
+ }
+ })
+ })
+ })
})
JS_ARRAY_EXPRESSION,
JS_FUNCTION_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
- JS_CONDITIONAL_EXPRESSION
+ JS_CONDITIONAL_EXPRESSION,
+ JS_CACHE_EXPRESSION
}
export const enum ElementTypes {
components: string[]
directives: string[]
hoists: JSChildNode[]
+ cached: number
codegenNode: TemplateChildNode | JSChildNode | undefined
}
| FunctionExpression
| ConditionalExpression
| SequenceExpression
+ | CacheExpression
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
alternate: JSChildNode
}
+export interface CacheExpression extends Node {
+ type: NodeTypes.JS_CACHE_EXPRESSION
+ index: number
+ value: JSChildNode
+}
+
// Codegen Node Types ----------------------------------------------------------
// createVNode(...)
loc: locStub
}
}
+
+export function createCacheExpression(
+ index: number,
+ value: JSChildNode
+): CacheExpression {
+ return {
+ type: NodeTypes.JS_CACHE_EXPRESSION,
+ index,
+ value,
+ loc: locStub
+ }
+}
SimpleExpressionNode,
FunctionExpression,
SequenceExpression,
- ConditionalExpression
+ ConditionalExpression,
+ CacheExpression
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import {
}
}
genHoists(ast.hoists, context)
+ genCached(ast.cached, context)
newline()
push(`return `)
} else {
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
genHoists(ast.hoists, context)
+ genCached(ast.cached, context)
newline()
push(`export default `)
}
})
}
+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) {
return (
isString(n) ||
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
+ case NodeTypes.JS_CACHE_EXPRESSION:
+ genCacheExpression(node, context)
+ break
/* istanbul ignore next */
default:
if (__DEV__) {
genNodeList(node.expressions, context)
context.push(`)`)
}
+
+function genCacheExpression(node: CacheExpression, context: CodegenContext) {
+ context.push(`_cached_${node.index} || (_cached_${node.index} = `)
+ genNode(node.value, context)
+ context.push(`)`)
+}
ElementTypes,
ElementCodegenNode,
ComponentCodegenNode,
- createCallExpression
+ createCallExpression,
+ CacheExpression,
+ createCacheExpression
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
export type DirectiveTransform = (
dir: DirectiveNode,
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[]
needRuntime: boolean | symbol
}
directiveTransforms?: { [name: string]: DirectiveTransform }
prefixIdentifiers?: boolean
hoistStatic?: boolean
+ cacheHandlers?: boolean
onError?: (error: CompilerError) => void
}
components: Set<string>
directives: Set<string>
hoists: JSChildNode[]
+ cached: number
identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: JSChildNode): SimpleExpressionNode
+ cache<T extends JSChildNode>(exp: T): CacheExpression | T
}
function createTransformContext(
{
prefixIdentifiers = false,
hoistStatic = false,
+ cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
onError = defaultOnError
components: new Set(),
directives: new Set(),
hoists: [],
+ cached: 0,
identifiers: {},
scopes: {
vFor: 0,
},
prefixIdentifiers,
hoistStatic,
+ cacheHandlers,
nodeTransforms,
directiveTransforms,
onError,
false,
exp.loc
)
+ },
+ cache(exp) {
+ if (cacheHandlers) {
+ context.cached++
+ return createCacheExpression(context.cached, exp)
+ } else {
+ return exp
+ }
}
}
root.components = [...context.components]
root.directives = [...context.directives]
root.hoists = context.hoists
+ root.cached = context.cached
}
export function traverseChildren(
PlainElementNode,
ComponentNode,
TemplateNode,
- ElementNode
+ ElementNode,
+ PlainElementCodegenNode
} from '../ast'
import { TransformContext } from '../transform'
import { WITH_DIRECTIVES } from '../runtimeHelpers'
import { PatchFlags, isString, isSymbol } from '@vue/shared'
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) {
walk(
root.children,
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
+ const hasBailoutProp = hasDynamicKeyOrRef(child) || hasCachedProps(child)
if (
!doNotHoistNode &&
- isStaticNode(child, resultCache) &&
- !hasDynamicKeyOrRef(child)
+ !hasBailoutProp &&
+ isStaticNode(child, resultCache)
) {
// whole tree is static
child.codegenNode = context.hoist(child.codegenNode!)
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
- !hasDynamicKeyOrRef(child)
+ !hasBailoutProp
) {
- let codegenNode = child.codegenNode as ElementCodegenNode
- if (codegenNode.callee === WITH_DIRECTIVES) {
- codegenNode = codegenNode.arguments[0]
- }
- const props = codegenNode.arguments[1]
+ const props = getNodeProps(child)
if (props && props !== `null`) {
- codegenNode.arguments[1] = context.hoist(props)
+ getVNodeCall(child).arguments[1] = context.hoist(props)
}
}
}
}
}
-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(
node: TemplateChildNode | SimpleExpressionNode,
resultCache: Map<TemplateChildNode, boolean> = new Map()
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]
+}
const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
if (
- (value.type === NodeTypes.SIMPLE_EXPRESSION ||
+ value.type === NodeTypes.JS_CACHE_EXPRESSION ||
+ ((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) &&
- isStaticNode(value)
+ isStaticNode(value))
) {
return
}
-import { DirectiveTransform, TransformContext } from '../transform'
+import { DirectiveTransform } from '../transform'
import {
createSimpleExpression,
createObjectProperty,
createCompoundExpression,
NodeTypes,
Property,
- CompoundExpressionNode,
- createInterpolation,
ElementTypes
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
-import { isMemberExpression, isSimpleIdentifier } from '../utils'
-import { isObject } from '@vue/shared'
+import { isMemberExpression, isSimpleIdentifier, hasScopeRef } from '../utils'
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir
])
: 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 = [
// modelValue: foo
createObjectProperty(propName, dir.exp!),
eventName,
createCompoundExpression([
`$event => (`,
- ...assignmentChildren,
+ ...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
` = $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 }
if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) {
const modifiers = dir.modifiers
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[] = []) {
return { props, needRuntime: false }
}
-import { DirectiveTransform } from '../transform'
+import { DirectiveTransform, DirectiveTransformResult } from '../transform'
import {
DirectiveNode,
createObjectProperty,
import { capitalize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
-import { isMemberExpression } from '../utils'
+import { isMemberExpression, hasScopeRef } from '../utils'
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
export const transformOn: DirectiveTransform = (
dir: VOnDirectiveNode,
node,
- context
+ context,
+ augmentor
) => {
const { loc, modifiers, arg } = dir
if (!dir.exp && !modifiers.length) {
eventName.children.unshift(`"on" + (`)
eventName.children.push(`)`)
}
- // TODO .once modifier handling since it is platform agnostic
- // other modifiers are handled in compiler-dom
// handler processing
let exp: ExpressionNode | undefined = dir.exp
+ let isCacheable: boolean = !exp
if (exp) {
- const isInlineStatement = !(
- isMemberExpression(exp.content) || fnExpRE.test(exp.content)
- )
+ const isMemberExp = isMemberExpression(exp.content)
+ const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
+
// process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) {
context.addIdentifiers(`$event`)
exp = processExpression(exp, context)
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
exp = createCompoundExpression([
`$event => (`,
}
}
- return {
+ let ret: DirectiveTransformResult = {
props: [
createObjectProperty(
eventName,
],
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
}
FunctionExpression,
CallExpression,
createCallExpression,
- createArrayExpression,
- IfBranchNode
+ createArrayExpression
} from '../ast'
import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors'
-import {
- findDir,
- isTemplateNode,
- assert,
- isVSlot,
- isSimpleIdentifier
-} from '../utils'
+import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor'
-import { isObject } from '@vue/shared'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
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
- }
-}
ElementCodegenNode,
SlotOutletCodegenNode,
ComponentCodegenNode,
- ExpressionNode
+ ExpressionNode,
+ IfBranchNode
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
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
// lazy require dependencies so that they don't end up in rollup's dep graph
export function isEmptyExpression(node: ExpressionNode) {
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
+ }
+}
ElementNode,
ObjectExpression,
CallExpression,
- NodeTypes,
- Property
+ NodeTypes
} from '@vue/compiler-core'
import { transformOn } from '../../src/transforms/vOn'
import { V_ON_WITH_MODIFIERS, V_ON_WITH_KEYS } from '../../src/runtimeHelpers'
import { transformExpression } from '../../../compiler-core/src/transforms/transformExpression'
import { createObjectMatcher } from '../../../compiler-core/__tests__/testUtils'
-function parseVOnProperties(
- template: string,
- options: CompilerOptions = {}
-): Property[] {
+function parseWithVOn(template: string, options: CompilerOptions = {}) {
const ast = parse(template)
transform(ast, {
nodeTransforms: [transformExpression, transformElement],
},
...options
})
- return (((ast.children[0] as ElementNode).codegenNode as CallExpression)
- .arguments[1] as ObjectExpression).properties
+ return {
+ root: ast,
+ props: (((ast.children[0] as ElementNode).codegenNode as CallExpression)
+ .arguments[1] as ObjectExpression).properties
+ }
}
describe('compiler-dom: transform v-on', () => {
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
})
expect(prop).toMatchObject({
})
it('should support multiple modifiers and event options w/ prefixIdentifiers: true', () => {
- const [prop] = parseVOnProperties(
- `<div @click.stop.capture.passive="test"/>`,
- { prefixIdentifiers: true }
- )
+ const {
+ props: [prop]
+ } = parseWithVOn(`<div @click.stop.capture.passive="test"/>`, {
+ prefixIdentifiers: true
+ })
expect(prop).toMatchObject({
type: NodeTypes.JS_PROPERTY,
value: createObjectMatcher({
options: createObjectMatcher({
capture: { 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', () => {
- const [prop] = parseVOnProperties(
- `<div @keyDown.stop.capture.ctrl.a="test"/>`,
- { prefixIdentifiers: true }
- )
+ const {
+ props: [prop]
+ } = parseWithVOn(`<div @keyDown.stop.capture.ctrl.a="test"/>`, {
+ prefixIdentifiers: true
+ })
expect(prop).toMatchObject({
type: NodeTypes.JS_PROPERTY,
value: createObjectMatcher({
},
options: createObjectMatcher({
capture: { content: 'true', isStatic: false }
- }),
- persistent: { content: 'true', isStatic: false }
+ })
})
})
})
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
})
expect(prop).toMatchObject({
})
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
})
expect(prop).toMatchObject({
}
})
})
+
+ 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 }
+ }
+ ]
+ }
+ })
+ })
})
)
export const transformOn: DirectiveTransform = (dir, node, context) => {
- const { modifiers } = dir
- const baseResult = baseTransform(dir, node, context)
- if (!modifiers.length) return baseResult
+ return baseTransform(dir, node, context, baseResult => {
+ const { modifiers } = dir
+ if (!modifiers.length) return baseResult
- let { key, value: handlerExp } = baseResult.props[0]
+ let { key, value: handlerExp } = baseResult.props[0]
- // modifiers for addEventListener() options, e.g. .passive & .capture
- const eventOptionModifiers = modifiers.filter(isEventOptionModifier)
- // modifiers that needs runtime guards
- const runtimeModifiers = modifiers.filter(m => !isEventOptionModifier(m))
+ // modifiers for addEventListener() options, e.g. .passive & .capture
+ const eventOptionModifiers = modifiers.filter(isEventOptionModifier)
+ // modifiers that needs runtime guards
+ const runtimeModifiers = modifiers.filter(m => !isEventOptionModifier(m))
- // built-in modifiers that are not keys
- const nonKeyModifiers = runtimeModifiers.filter(isNonKeyModifier)
- if (nonKeyModifiers.length) {
- handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
- handlerExp,
- JSON.stringify(nonKeyModifiers)
- ])
- }
+ // built-in modifiers that are not keys
+ const nonKeyModifiers = runtimeModifiers.filter(isNonKeyModifier)
+ if (nonKeyModifiers.length) {
+ handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
+ handlerExp,
+ JSON.stringify(nonKeyModifiers)
+ ])
+ }
- const keyModifiers = runtimeModifiers.filter(m => !isNonKeyModifier(m))
- if (
- keyModifiers.length &&
- // if event name is dynamic, always wrap with keys guard
- (key.type === NodeTypes.COMPOUND_EXPRESSION ||
- !key.isStatic ||
- isKeyboardEvent(key.content))
- ) {
- handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
- handlerExp,
- JSON.stringify(keyModifiers)
- ])
- }
+ const keyModifiers = runtimeModifiers.filter(m => !isNonKeyModifier(m))
+ if (
+ keyModifiers.length &&
+ // if event name is dynamic, always wrap with keys guard
+ (key.type === NodeTypes.COMPOUND_EXPRESSION ||
+ !key.isStatic ||
+ isKeyboardEvent(key.content))
+ ) {
+ handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
+ handlerExp,
+ JSON.stringify(keyModifiers)
+ ])
+ }
- if (eventOptionModifiers.length) {
- handlerExp = createObjectExpression([
- createObjectProperty('handler', handlerExp),
- createObjectProperty(
- 'options',
- createObjectExpression(
- eventOptionModifiers.map(modifier =>
- createObjectProperty(
- modifier,
- createSimpleExpression('true', false)
+ if (eventOptionModifiers.length) {
+ handlerExp = createObjectExpression([
+ createObjectProperty('handler', handlerExp),
+ createObjectProperty(
+ 'options',
+ createObjectExpression(
+ eventOptionModifiers.map(modifier =>
+ createObjectProperty(
+ modifier,
+ createSimpleExpression('true', false)
+ )
)
)
)
- ),
- // so the runtime knows the options never change
- createObjectProperty('persistent', createSimpleExpression('true', false))
- ])
- }
+ ])
+ }
- return {
- props: [createObjectProperty(key, handlerExp)],
- needRuntime: false
- }
+ return {
+ props: [createObjectProperty(key, handlerExp)],
+ needRuntime: false
+ }
+ })
}
type EventValueWithOptions = {
handler: EventValue
options: AddEventListenerOptions
- persistent?: boolean
invoker?: Invoker | null
}
const invoker = prevValue && prevValue.invoker
const value =
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 next = nextOptions || EMPTY_OBJ
if (
export const compilerOptions: CompilerOptions = reactive({
mode: 'module',
prefixIdentifiers: false,
- hoistStatic: false
+ hoistStatic: false,
+ cacheHandlers: false
})
const App = {
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')
])
]
}