diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index e582f98a..309a8440 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -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 { 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)`) + }) }) diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap index 859db344..8cbe9fc4 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap @@ -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 diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap index 48c22aef..ea11cf71 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vModel.spec.ts.snap @@ -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\\"])) }" `; diff --git a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts index 8632ce95..3236276b 100644 --- a/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts +++ b/packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts @@ -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(`
`, { + prefixIdentifiers: true, + cacheHandlers: true + }) + + expect(root.cached).toBe(1) + expect(root.hoists.length).toBe(0) + expect(generate(root).code).toMatchSnapshot() + }) }) }) diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts index 731da1e7..10ee0641 100644 --- a/packages/compiler-core/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts @@ -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('', { - 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( '', { - 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', () => { diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts index 41bd6677..57b7c4f8 100644 --- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts @@ -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(`
`) + const { node } = parseWithVOn(`
`) 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(`
`) + const { node } = parseWithVOn(`
`) 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(`
`, { + const { node } = parseWithVOn(`
`, { 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(`
`, { + const { node } = parseWithVOn(`
`, { 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(`
`) + const { node } = parseWithVOn(`
`) 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(`
`, { + const { node } = parseWithVOn(`
`, { 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(`
`) + const { node } = parseWithVOn(`
`) 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(`
`) + const { node } = parseWithVOn(`
`) 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(`
`, { + const { node } = parseWithVOn(`
`, { 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(`
`, { + const { node } = parseWithVOn(`
`, { 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(`
`, { + 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(`
`, { + 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(`
`, { + 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(`
`, { + 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` }, `++`, `)`] + } + }) + }) + }) }) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 5813f363..e2faabc9 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -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 + } +} diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index ad896ddc..8748e0bc 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -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(`)`) +} diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index ae3e9e7a..7ecf4ac4 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -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 { components: Set directives: Set hoists: JSChildNode[] + cached: number identifiers: { [name: string]: number | undefined } scopes: { vFor: number @@ -91,6 +100,7 @@ export interface TransformContext extends Required { addIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void hoist(exp: JSChildNode): SimpleExpressionNode + cache(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( diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 87e2a136..75bcfb39 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -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 = 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] +} diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 970173ff..646a51ce 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -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 } diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index dd462cf9..81c91720 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -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 } } diff --git a/packages/compiler-core/src/transforms/vOn.ts b/packages/compiler-core/src/transforms/vOn.ts index dbdc6275..5f5af20f 100644 --- a/packages/compiler-core/src/transforms/vOn.ts +++ b/packages/compiler-core/src/transforms/vOn.ts @@ -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 } diff --git a/packages/compiler-core/src/transforms/vSlot.ts b/packages/compiler-core/src/transforms/vSlot.ts index 94089f29..bb432792 100644 --- a/packages/compiler-core/src/transforms/vSlot.ts +++ b/packages/compiler-core/src/transforms/vSlot.ts @@ -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 - } -} diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts index b2f552bc..aed08458 100644 --- a/packages/compiler-core/src/utils.ts +++ b/packages/compiler-core/src/utils.ts @@ -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 + } +} diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts index 77b35a0d..0ac528ea 100644 --- a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts @@ -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(`
`, { + const { + props: [prop] + } = parseWithVOn(`
`, { 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( - `
`, - { prefixIdentifiers: true } - ) + const { + props: [prop] + } = parseWithVOn(`
`, { + 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( - `
`, - { prefixIdentifiers: true } - ) + const { + props: [prop] + } = parseWithVOn(`
`, { + 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(`
`, { + const { + props: [prop] + } = parseWithVOn(`
`, { 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(`
`, { + const { + props: [prop] + } = parseWithVOn(`
`, { 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(`
`, { + 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 } + } + ] + } + }) + }) }) diff --git a/packages/compiler-dom/src/transforms/vOn.ts b/packages/compiler-dom/src/transforms/vOn.ts index 53085d85..2fac2da3 100644 --- a/packages/compiler-dom/src/transforms/vOn.ts +++ b/packages/compiler-dom/src/transforms/vOn.ts @@ -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 + } + }) } diff --git a/packages/runtime-dom/src/modules/events.ts b/packages/runtime-dom/src/modules/events.ts index de0a3714..7de6f1ad 100644 --- a/packages/runtime-dom/src/modules/events.ts +++ b/packages/runtime-dom/src/modules/events.ts @@ -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 ( diff --git a/packages/template-explorer/src/options.ts b/packages/template-explorer/src/options.ts index fcf518cf..64fe809d 100644 --- a/packages/template-explorer/src/options.ts +++ b/packages/template-explorer/src/options.ts @@ -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 = (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 = (e.target).checked + } + }), + h('label', { for: 'cache' }, 'cacheHandlers') ]) ] }