]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(v-on): cache handlers
authorEvan You <yyx990803@gmail.com>
Sat, 19 Oct 2019 01:51:34 +0000 (21:51 -0400)
committerEvan You <yyx990803@gmail.com>
Sat, 19 Oct 2019 01:51:34 +0000 (21:51 -0400)
19 files changed:
packages/compiler-core/__tests__/codegen.spec.ts
packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap
packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
packages/compiler-core/__tests__/transforms/vModel.spec.ts
packages/compiler-core/__tests__/transforms/vOn.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-core/src/transforms/vOn.ts
packages/compiler-core/src/transforms/vSlot.ts
packages/compiler-core/src/utils.ts
packages/compiler-dom/__tests__/transforms/vOn.spec.ts
packages/compiler-dom/src/transforms/vOn.ts
packages/runtime-dom/src/modules/events.ts
packages/template-explorer/src/options.ts

index e582f98a0df146c72d7cf94de73fb74663f04df5..309a8440798604fdb914941909004da8560eb14e 100644 (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)`)
+  })
 })
index 859db34470d84b5659231a56b8b94faca51309e7..8cbe9fc467fde9cb2d25e13cf350a85535b3c95e 100644 (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
 
index 48c22aef59c4c7d0b1ba4a6437b2e744677abba9..ea11cf7151991790709bb0dfc188274416063a95 100644 (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\\"]))
 }"
 `;
 
index 8632ce95a33b1339902b1093620ca3a19cc7e102..3236276b0ae34ca793e75461c3cfae8e54ab8a1c 100644 (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()
+    })
   })
 })
index 731da1e7dcda6a6e4f8b572f75da352eb58420fd..10ee06413fd133ffbc69871bffa715b0437a3ee1 100644 (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', () => {
index 41bd6677043ce60cc47eea6aa805c346f486f57d..57b7c4f89252ff905f9c8d3a931608b4b252dd90 100644 (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` }, `++`, `)`]
+        }
+      })
+    })
+  })
 })
index 5813f363248734e6c2ef7f2a2fea616771b45d2a..e2faabc974237ed443b022ccc671b7017e1271e1 100644 (file)
@@ -42,7 +42,8 @@ export const enum NodeTypes {
   JS_ARRAY_EXPRESSION,
   JS_FUNCTION_EXPRESSION,
   JS_SEQUENCE_EXPRESSION,
-  JS_CONDITIONAL_EXPRESSION
+  JS_CONDITIONAL_EXPRESSION,
+  JS_CACHE_EXPRESSION
 }
 
 export const enum ElementTypes {
@@ -93,6 +94,7 @@ export interface RootNode extends Node {
   components: string[]
   directives: string[]
   hoists: JSChildNode[]
+  cached: number
   codegenNode: TemplateChildNode | JSChildNode | undefined
 }
 
@@ -236,6 +238,7 @@ export type JSChildNode =
   | FunctionExpression
   | ConditionalExpression
   | SequenceExpression
+  | CacheExpression
 
 export interface CallExpression extends Node {
   type: NodeTypes.JS_CALL_EXPRESSION
@@ -283,6 +286,12 @@ export interface ConditionalExpression extends Node {
   alternate: JSChildNode
 }
 
+export interface CacheExpression extends Node {
+  type: NodeTypes.JS_CACHE_EXPRESSION
+  index: number
+  value: JSChildNode
+}
+
 // Codegen Node Types ----------------------------------------------------------
 
 // createVNode(...)
@@ -605,3 +614,15 @@ export function createConditionalExpression(
     loc: locStub
   }
 }
+
+export function createCacheExpression(
+  index: number,
+  value: JSChildNode
+): CacheExpression {
+  return {
+    type: NodeTypes.JS_CACHE_EXPRESSION,
+    index,
+    value,
+    loc: locStub
+  }
+}
index ad896ddc8ecbd41668157000d575f979e687355b..8748e0bcecf9ab429f1ac55e7c65ff504e3f3a66 100644 (file)
@@ -16,7 +16,8 @@ import {
   SimpleExpressionNode,
   FunctionExpression,
   SequenceExpression,
-  ConditionalExpression
+  ConditionalExpression,
+  CacheExpression
 } from './ast'
 import { SourceMapGenerator, RawSourceMap } from 'source-map'
 import {
@@ -218,6 +219,7 @@ export function generate(
       }
     }
     genHoists(ast.hoists, context)
+    genCached(ast.cached, context)
     newline()
     push(`return `)
   } else {
@@ -226,6 +228,7 @@ export function generate(
       push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
     }
     genHoists(ast.hoists, context)
+    genCached(ast.cached, context)
     newline()
     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) {
   return (
     isString(n) ||
@@ -419,6 +434,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
     case NodeTypes.JS_CONDITIONAL_EXPRESSION:
       genConditionalExpression(node, context)
       break
+    case NodeTypes.JS_CACHE_EXPRESSION:
+      genCacheExpression(node, context)
+      break
     /* istanbul ignore next */
     default:
       if (__DEV__) {
@@ -612,3 +630,9 @@ function genSequenceExpression(
   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(`)`)
+}
index ae3e9e7a02b8e650906f7f170344ae48789f73df..7ecf4ac41c0b1a48b88c29d51b368fa3c357442f 100644 (file)
@@ -13,7 +13,9 @@ import {
   ElementTypes,
   ElementCodegenNode,
   ComponentCodegenNode,
-  createCallExpression
+  createCallExpression,
+  CacheExpression,
+  createCacheExpression
 } from './ast'
 import { isString, isArray } from '@vue/shared'
 import { CompilerError, defaultOnError } from './errors'
@@ -45,8 +47,13 @@ export type NodeTransform = (
 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
 }
@@ -64,6 +71,7 @@ export interface TransformOptions {
   directiveTransforms?: { [name: string]: DirectiveTransform }
   prefixIdentifiers?: boolean
   hoistStatic?: boolean
+  cacheHandlers?: boolean
   onError?: (error: CompilerError) => void
 }
 
@@ -73,6 +81,7 @@ export interface TransformContext extends Required<TransformOptions> {
   components: Set<string>
   directives: Set<string>
   hoists: JSChildNode[]
+  cached: number
   identifiers: { [name: string]: number | undefined }
   scopes: {
     vFor: number
@@ -91,6 +100,7 @@ export interface TransformContext extends Required<TransformOptions> {
   addIdentifiers(exp: ExpressionNode | string): void
   removeIdentifiers(exp: ExpressionNode | string): void
   hoist(exp: JSChildNode): SimpleExpressionNode
+  cache<T extends JSChildNode>(exp: T): CacheExpression | T
 }
 
 function createTransformContext(
@@ -98,6 +108,7 @@ function createTransformContext(
   {
     prefixIdentifiers = false,
     hoistStatic = false,
+    cacheHandlers = false,
     nodeTransforms = [],
     directiveTransforms = {},
     onError = defaultOnError
@@ -109,6 +120,7 @@ function createTransformContext(
     components: new Set(),
     directives: new Set(),
     hoists: [],
+    cached: 0,
     identifiers: {},
     scopes: {
       vFor: 0,
@@ -118,6 +130,7 @@ function createTransformContext(
     },
     prefixIdentifiers,
     hoistStatic,
+    cacheHandlers,
     nodeTransforms,
     directiveTransforms,
     onError,
@@ -204,6 +217,14 @@ function createTransformContext(
         false,
         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.directives = [...context.directives]
   root.hoists = context.hoists
+  root.cached = context.cached
 }
 
 export function traverseChildren(
index 87e2a1362dff080e474acb51bc384cc21b3f6c60..75bcfb3985be3c0046a443b4a7cbb5f5a2874dfd 100644 (file)
@@ -8,17 +8,14 @@ import {
   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,
@@ -53,10 +50,11 @@ function walk(
       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!)
@@ -69,15 +67,11 @@ function walk(
           (!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)
           }
         }
       }
@@ -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(
   node: TemplateChildNode | SimpleExpressionNode,
   resultCache: Map<TemplateChildNode, boolean> = new Map()
@@ -157,3 +142,51 @@ export function isStaticNode(
       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]
+}
index 970173ff3c655a13206e5861ad38fcf6330b74dc..646a51ce61931b53e5c042f595789602ac1e4e87 100644 (file)
@@ -222,9 +222,10 @@ export function buildProps(
   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
       }
index dd462cf927bc33dbe310b60e9c70f7d1f541a3f8..81c917203e3ceee4037ab6114d6b6d7766769f32 100644 (file)
@@ -1,17 +1,14 @@
-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
@@ -54,16 +51,6 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
         ])
     : 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!),
@@ -72,12 +59,21 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
       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
@@ -94,30 +90,6 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
   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 }
 }
index dbdc6275315274e74f83e2c6198cd0b452c6731d..5f5af20f69b50ad969d6fe583b9844a40db7aad2 100644 (file)
@@ -1,4 +1,4 @@
-import { DirectiveTransform } from '../transform'
+import { DirectiveTransform, DirectiveTransformResult } from '../transform'
 import {
   DirectiveNode,
   createObjectProperty,
@@ -11,7 +11,7 @@ import {
 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*\(/
 
@@ -28,7 +28,8 @@ export interface VOnDirectiveNode extends DirectiveNode {
 export const transformOn: DirectiveTransform = (
   dir: VOnDirectiveNode,
   node,
-  context
+  context,
+  augmentor
 ) => {
   const { loc, modifiers, arg } = dir
   if (!dir.exp && !modifiers.length) {
@@ -51,22 +52,37 @@ export const transformOn: DirectiveTransform = (
     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 => (`,
@@ -76,7 +92,7 @@ export const transformOn: DirectiveTransform = (
     }
   }
 
-  return {
+  let ret: DirectiveTransformResult = {
     props: [
       createObjectProperty(
         eventName,
@@ -85,4 +101,18 @@ export const transformOn: DirectiveTransform = (
     ],
     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
 }
index 94089f297fb911d842543f6b2e154853067937db..bb43279223c05b51f0e14d9abc6abcf1c15ee747 100644 (file)
@@ -19,21 +19,13 @@ import {
   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
@@ -337,49 +329,3 @@ function buildDynamicSlot(
     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
-  }
-}
index b2f552bcb0881d9bcc75d12ee1eb7a9fb4e0171a..aed084586aee277d4d93c4a8e95e0fd39ce68a49 100644 (file)
@@ -21,13 +21,14 @@ import {
   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
@@ -250,3 +251,51 @@ export function toValidAssetId(
 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
+  }
+}
index 77b35a0d3508c82a0973f759de9992fae8d44534..0ac528ea16ac3c37ad501988f5467b26d42efeba 100644 (file)
@@ -5,8 +5,7 @@ import {
   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'
@@ -14,10 +13,7 @@ import { transformElement } from '../../../compiler-core/src/transforms/transfor
 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],
@@ -26,13 +22,18 @@ function parseVOnProperties(
     },
     ...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({
@@ -45,10 +46,11 @@ describe('compiler-dom: transform v-on', () => {
   })
 
   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({
@@ -59,17 +61,17 @@ describe('compiler-dom: transform v-on', () => {
         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({
@@ -85,14 +87,15 @@ describe('compiler-dom: transform v-on', () => {
         },
         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({
@@ -105,7 +108,9 @@ describe('compiler-dom: transform v-on', () => {
   })
 
   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({
@@ -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 }
+          }
+        ]
+      }
+    })
+  })
 })
index 53085d854df3d85485d441480c00cdb7cdae31f7..2fac2da388452a43e33b086756a29681c48cdca5 100644 (file)
@@ -25,61 +25,60 @@ const isKeyboardEvent = /*#__PURE__*/ makeMap(
 )
 
 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
+    }
+  })
 }
index de0a3714f3b534f404d47e795b7f4e607473ea28..7de6f1add78f05605293086597467f8a7506478c 100644 (file)
@@ -17,7 +17,6 @@ type EventValue = (Function | Function[]) & {
 type EventValueWithOptions = {
   handler: EventValue
   options: AddEventListenerOptions
-  persistent?: boolean
   invoker?: Invoker | null
 }
 
@@ -77,10 +76,8 @@ export function patchEvent(
   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 (
index fcf518cf7bf6fea0359dea462be5ec46d8b17048..64fe809d7e62c66828be7e70fc1ec2abb540afa3 100644 (file)
@@ -4,7 +4,8 @@ import { CompilerOptions } from '@vue/compiler-dom'
 export const compilerOptions: CompilerOptions = reactive({
   mode: 'module',
   prefixIdentifiers: false,
-  hoistStatic: false
+  hoistStatic: false,
+  cacheHandlers: false
 })
 
 const App = {
@@ -70,7 +71,20 @@ 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')
       ])
     ]
   }