feat(v-on): cache handlers
This commit is contained in:
parent
39ea67a2d2
commit
58593c4714
@ -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)`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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\\"]))
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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', () => {
|
||||||
|
@ -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` }, `++`, `)`]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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(`)`)
|
||||||
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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]
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
.arguments[1] as ObjectExpression).properties
|
root: ast,
|
||||||
|
props: (((ast.children[0] as ElementNode).codegenNode as CallExpression)
|
||||||
|
.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 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -25,61 +25,60 @@ const isKeyboardEvent = /*#__PURE__*/ makeMap(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const transformOn: DirectiveTransform = (dir, node, context) => {
|
export const transformOn: DirectiveTransform = (dir, node, context) => {
|
||||||
const { modifiers } = dir
|
return baseTransform(dir, node, context, baseResult => {
|
||||||
const baseResult = baseTransform(dir, node, context)
|
const { modifiers } = dir
|
||||||
if (!modifiers.length) return baseResult
|
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
|
// modifiers for addEventListener() options, e.g. .passive & .capture
|
||||||
const eventOptionModifiers = modifiers.filter(isEventOptionModifier)
|
const eventOptionModifiers = modifiers.filter(isEventOptionModifier)
|
||||||
// modifiers that needs runtime guards
|
// modifiers that needs runtime guards
|
||||||
const runtimeModifiers = modifiers.filter(m => !isEventOptionModifier(m))
|
const runtimeModifiers = modifiers.filter(m => !isEventOptionModifier(m))
|
||||||
|
|
||||||
// built-in modifiers that are not keys
|
// built-in modifiers that are not keys
|
||||||
const nonKeyModifiers = runtimeModifiers.filter(isNonKeyModifier)
|
const nonKeyModifiers = runtimeModifiers.filter(isNonKeyModifier)
|
||||||
if (nonKeyModifiers.length) {
|
if (nonKeyModifiers.length) {
|
||||||
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
|
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
|
||||||
handlerExp,
|
handlerExp,
|
||||||
JSON.stringify(nonKeyModifiers)
|
JSON.stringify(nonKeyModifiers)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyModifiers = runtimeModifiers.filter(m => !isNonKeyModifier(m))
|
const keyModifiers = runtimeModifiers.filter(m => !isNonKeyModifier(m))
|
||||||
if (
|
if (
|
||||||
keyModifiers.length &&
|
keyModifiers.length &&
|
||||||
// if event name is dynamic, always wrap with keys guard
|
// if event name is dynamic, always wrap with keys guard
|
||||||
(key.type === NodeTypes.COMPOUND_EXPRESSION ||
|
(key.type === NodeTypes.COMPOUND_EXPRESSION ||
|
||||||
!key.isStatic ||
|
!key.isStatic ||
|
||||||
isKeyboardEvent(key.content))
|
isKeyboardEvent(key.content))
|
||||||
) {
|
) {
|
||||||
handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
|
handlerExp = createCallExpression(context.helper(V_ON_WITH_KEYS), [
|
||||||
handlerExp,
|
handlerExp,
|
||||||
JSON.stringify(keyModifiers)
|
JSON.stringify(keyModifiers)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventOptionModifiers.length) {
|
if (eventOptionModifiers.length) {
|
||||||
handlerExp = createObjectExpression([
|
handlerExp = createObjectExpression([
|
||||||
createObjectProperty('handler', handlerExp),
|
createObjectProperty('handler', handlerExp),
|
||||||
createObjectProperty(
|
createObjectProperty(
|
||||||
'options',
|
'options',
|
||||||
createObjectExpression(
|
createObjectExpression(
|
||||||
eventOptionModifiers.map(modifier =>
|
eventOptionModifiers.map(modifier =>
|
||||||
createObjectProperty(
|
createObjectProperty(
|
||||||
modifier,
|
modifier,
|
||||||
createSimpleExpression('true', false)
|
createSimpleExpression('true', false)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
])
|
||||||
// so the runtime knows the options never change
|
}
|
||||||
createObjectProperty('persistent', createSimpleExpression('true', false))
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: [createObjectProperty(key, handlerExp)],
|
props: [createObjectProperty(key, handlerExp)],
|
||||||
needRuntime: false
|
needRuntime: false
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
@ -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')
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user