diff --git a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap index 851147e0f..ae00fd8de 100644 --- a/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap @@ -21,6 +21,20 @@ export default function render() { }" `; +exports[`compiler: codegen CacheExpression w/ isVNode: true 1`] = ` +" +export default function render() { + const _ctx = this + const _cache = _ctx.$cache + return _cache[1] || ( + setBlockTracking(-1), + _cache[1] = foo, + setBlockTracking(1), + _cache[1] + ) +}" +`; + exports[`compiler: codegen ConditionalExpression 1`] = ` " return function render() { diff --git a/packages/compiler-core/__tests__/codegen.spec.ts b/packages/compiler-core/__tests__/codegen.spec.ts index 9d7f0365f..d9de26546 100644 --- a/packages/compiler-core/__tests__/codegen.spec.ts +++ b/packages/compiler-core/__tests__/codegen.spec.ts @@ -380,4 +380,33 @@ describe('compiler: codegen', () => { expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`) expect(code).toMatchSnapshot() }) + + test('CacheExpression w/ isVNode: true', () => { + const { code } = generate( + createRoot({ + cached: 1, + codegenNode: createCacheExpression( + 1, + createSimpleExpression(`foo`, false), + true + ) + }), + { + mode: 'module', + prefixIdentifiers: true + } + ) + expect(code).toMatch(`const _cache = _ctx.$cache`) + expect(code).toMatch( + ` + _cache[1] || ( + setBlockTracking(-1), + _cache[1] = foo, + setBlockTracking(1), + _cache[1] + ) + `.trim() + ) + expect(code).toMatchSnapshot() + }) }) diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap new file mode 100644 index 000000000..4c6312799 --- /dev/null +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`compiler: v-once transform as root node 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { setBlockTracking: _setBlockTracking, createVNode: _createVNode } = _Vue + const _cache = $cache + + return _cache[1] || ( + _setBlockTracking(-1), + _cache[1] = _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]), + _setBlockTracking(1), + _cache[1] + ) + } +}" +`; + +exports[`compiler: v-once transform on component 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { setBlockTracking: _setBlockTracking, resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue + const _cache = $cache + + const _component_Comp = _resolveComponent(\\"Comp\\") + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _cache[1] || ( + _setBlockTracking(-1), + _cache[1] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, [\\"id\\"]), + _setBlockTracking(1), + _cache[1] + ) + ])) + } +}" +`; + +exports[`compiler: v-once transform on nested plain element 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue + const _cache = $cache + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _cache[1] || ( + _setBlockTracking(-1), + _cache[1] = _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]), + _setBlockTracking(1), + _cache[1] + ) + ])) + } +}" +`; + +exports[`compiler: v-once transform on slot outlet 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue + const _cache = $cache + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _cache[1] || ( + _setBlockTracking(-1), + _cache[1] = _renderSlot($slots, \\"default\\"), + _setBlockTracking(1), + _cache[1] + ) + ])) + } +}" +`; + +exports[`compiler: v-once transform with hoistStatic: true 1`] = ` +"const _Vue = Vue + +return function render() { + with (this) { + const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue + const _cache = $cache + + return (_openBlock(), _createBlock(\\"div\\", null, [ + _cache[1] || ( + _setBlockTracking(-1), + _cache[1] = _createVNode(\\"div\\"), + _setBlockTracking(1), + _cache[1] + ) + ])) + } +}" +`; diff --git a/packages/compiler-core/__tests__/transforms/vModel.spec.ts b/packages/compiler-core/__tests__/transforms/vModel.spec.ts index 10ee06413..a44b16c80 100644 --- a/packages/compiler-core/__tests__/transforms/vModel.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vModel.spec.ts @@ -387,7 +387,8 @@ describe('compiler: transform v-model', () => { const root = parseWithVModel('', { prefixIdentifiers: true }) - const args = (root.children[0] as ComponentNode).codegenNode!.arguments + const args = ((root.children[0] as ComponentNode) + .codegenNode as CallExpression).arguments // props expect(args[1]).toMatchObject({ properties: [ diff --git a/packages/compiler-core/__tests__/transforms/vOnce.spec.ts b/packages/compiler-core/__tests__/transforms/vOnce.spec.ts index e0533b4f4..5d2a2a05f 100644 --- a/packages/compiler-core/__tests__/transforms/vOnce.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vOnce.spec.ts @@ -1,28 +1,109 @@ -import { parse, transform, ElementNode, CallExpression } from '../../src' +import { + parse, + transform, + NodeTypes, + generate, + CompilerOptions +} from '../../src' import { transformOnce } from '../../src/transforms/vOnce' import { transformElement } from '../../src/transforms/transformElement' -import { createObjectMatcher } from '../testUtils' +import { + CREATE_VNODE, + RENDER_SLOT, + SET_BLOCK_TRACKING +} from '../../src/runtimeHelpers' +import { transformBind } from '../../src/transforms/vBind' +import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet' -function transformWithOnce(template: string) { +function transformWithOnce(template: string, options: CompilerOptions = {}) { const ast = parse(template) transform(ast, { - nodeTransforms: [transformElement], + nodeTransforms: [transformOnce, transformElement, transformSlotOutlet], directiveTransforms: { - once: transformOnce - } + bind: transformBind + }, + ...options }) - return ast.children[0] as ElementNode + return ast } describe('compiler: v-once transform', () => { - test('should add no props to DOM', () => { - const node = transformWithOnce(`
`) - const codegenArgs = (node.codegenNode as CallExpression).arguments + test('as root node', () => { + const root = transformWithOnce(`
`) + expect(root.cached).toBe(1) + expect(root.helpers).toContain(SET_BLOCK_TRACKING) + expect(root.codegenNode).toMatchObject({ + type: NodeTypes.JS_CACHE_EXPRESSION, + index: 1, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE + } + }) + expect(generate(root).code).toMatchSnapshot() + }) - expect(codegenArgs[1]).toMatchObject( - createObjectMatcher({ - $once: `[true]` - }) - ) + test('on nested plain element', () => { + const root = transformWithOnce(`
`) + expect(root.cached).toBe(1) + expect(root.helpers).toContain(SET_BLOCK_TRACKING) + expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ + type: NodeTypes.JS_CACHE_EXPRESSION, + index: 1, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE + } + }) + expect(generate(root).code).toMatchSnapshot() + }) + + test('on component', () => { + const root = transformWithOnce(`
`) + expect(root.cached).toBe(1) + expect(root.helpers).toContain(SET_BLOCK_TRACKING) + expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ + type: NodeTypes.JS_CACHE_EXPRESSION, + index: 1, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE + } + }) + expect(generate(root).code).toMatchSnapshot() + }) + + test('on slot outlet', () => { + const root = transformWithOnce(`
`) + expect(root.cached).toBe(1) + expect(root.helpers).toContain(SET_BLOCK_TRACKING) + expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ + type: NodeTypes.JS_CACHE_EXPRESSION, + index: 1, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: RENDER_SLOT + } + }) + expect(generate(root).code).toMatchSnapshot() + }) + + // cached nodes should be ignored by hoistStatic transform + test('with hoistStatic: true', () => { + const root = transformWithOnce(`
`, { + hoistStatic: true + }) + expect(root.cached).toBe(1) + expect(root.helpers).toContain(SET_BLOCK_TRACKING) + expect(root.hoists.length).toBe(0) + expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ + type: NodeTypes.JS_CACHE_EXPRESSION, + index: 1, + value: { + type: NodeTypes.JS_CALL_EXPRESSION, + callee: CREATE_VNODE + } + }) + expect(generate(root).code).toMatchSnapshot() }) }) diff --git a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts index b25897d4d..4ba645cc9 100644 --- a/packages/compiler-core/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-core/__tests__/transforms/vSlot.spec.ts @@ -365,7 +365,7 @@ describe('compiler: transform component slots', () => { } else { const innerComp = (root.children[0] as ComponentNode) .children[0] as ComponentNode - flag = innerComp.codegenNode!.arguments[3] + flag = (innerComp.codegenNode as CallExpression).arguments[3] } if (shouldForce) { expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS)) diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts index 740da9b15..4a53de662 100644 --- a/packages/compiler-core/src/ast.ts +++ b/packages/compiler-core/src/ast.ts @@ -116,40 +116,45 @@ export interface BaseElementNode extends Node { isSelfClosing: boolean props: Array children: TemplateChildNode[] - codegenNode: CallExpression | SimpleExpressionNode | undefined + codegenNode: + | CallExpression + | SimpleExpressionNode + | CacheExpression + | undefined } export interface PlainElementNode extends BaseElementNode { tagType: ElementTypes.ELEMENT - codegenNode: ElementCodegenNode | undefined | SimpleExpressionNode // only when hoisted + codegenNode: + | ElementCodegenNode + | undefined + | SimpleExpressionNode // when hoisted + | CacheExpression // when cached by v-once } export interface ComponentNode extends BaseElementNode { tagType: ElementTypes.COMPONENT - codegenNode: ComponentCodegenNode | undefined + codegenNode: ComponentCodegenNode | undefined | CacheExpression // when cached by v-once } export interface SlotOutletNode extends BaseElementNode { tagType: ElementTypes.SLOT - codegenNode: SlotOutletCodegenNode | undefined + codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once } export interface TemplateNode extends BaseElementNode { tagType: ElementTypes.TEMPLATE - codegenNode: - | ElementCodegenNode - | CodegenNodeWithDirective - | undefined + codegenNode: ElementCodegenNode | undefined | CacheExpression } export interface PortalNode extends BaseElementNode { tagType: ElementTypes.PORTAL - codegenNode: ElementCodegenNode | undefined + codegenNode: ElementCodegenNode | undefined | CacheExpression } export interface SuspenseNode extends BaseElementNode { tagType: ElementTypes.SUSPENSE - codegenNode: ElementCodegenNode | undefined + codegenNode: ElementCodegenNode | undefined | CacheExpression } export interface TextNode extends Node { @@ -298,6 +303,7 @@ export interface CacheExpression extends Node { type: NodeTypes.JS_CACHE_EXPRESSION index: number value: JSChildNode + isVNode: boolean } // Codegen Node Types ---------------------------------------------------------- @@ -625,12 +631,14 @@ export function createConditionalExpression( export function createCacheExpression( index: number, - value: JSChildNode + value: JSChildNode, + isVNode: boolean = false ): CacheExpression { return { type: NodeTypes.JS_CACHE_EXPRESSION, index, value, + isVNode, loc: locStub } } diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts index ba49ad16d..04384ceb6 100644 --- a/packages/compiler-core/src/codegen.ts +++ b/packages/compiler-core/src/codegen.ts @@ -34,7 +34,8 @@ import { COMMENT, helperNameMap, RESOLVE_COMPONENT, - RESOLVE_DIRECTIVE + RESOLVE_DIRECTIVE, + SET_BLOCK_TRACKING } from './runtimeHelpers' type CodegenNode = TemplateChildNode | JSChildNode @@ -247,6 +248,10 @@ export function generate( .join(', ')} } = _Vue` ) newline() + if (ast.cached > 0) { + push(`const _cache = $cache`) + newline() + } newline() } } else { @@ -625,7 +630,22 @@ function genSequenceExpression( } function genCacheExpression(node: CacheExpression, context: CodegenContext) { - context.push(`_cache[${node.index}] || (_cache[${node.index}] = `) + const { push, helper, indent, deindent, newline } = context + push(`_cache[${node.index}] || (`) + if (node.isVNode) { + indent() + push(`${helper(SET_BLOCK_TRACKING)}(-1),`) + newline() + } + push(`_cache[${node.index}] = `) genNode(node.value, context) - context.push(`)`) + if (node.isVNode) { + push(`,`) + newline() + push(`${helper(SET_BLOCK_TRACKING)}(1),`) + newline() + push(`_cache[${node.index}]`) + deindent() + } + push(`)`) } diff --git a/packages/compiler-core/src/index.ts b/packages/compiler-core/src/index.ts index 097f89173..5b34d66eb 100644 --- a/packages/compiler-core/src/index.ts +++ b/packages/compiler-core/src/index.ts @@ -44,6 +44,7 @@ export function baseCompile( ...options, prefixIdentifiers, nodeTransforms: [ + transformOnce, transformIf, transformFor, ...(prefixIdentifiers @@ -62,7 +63,6 @@ export function baseCompile( directiveTransforms: { on: transformOn, bind: transformBind, - once: transformOnce, model: transformModel, ...(options.directiveTransforms || {}) // user transforms } diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts index 6cd690ced..a2ed747ba 100644 --- a/packages/compiler-core/src/runtimeHelpers.ts +++ b/packages/compiler-core/src/runtimeHelpers.ts @@ -19,6 +19,7 @@ export const TO_STRING = Symbol(__DEV__ ? `toString` : ``) export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``) export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``) export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) +export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) // Name mapping for runtime helpers that need to be imported from 'vue' in // generated code. Make sure these are correctly exported in the runtime! @@ -42,7 +43,8 @@ export const helperNameMap: any = { [TO_STRING]: `toString`, [MERGE_PROPS]: `mergeProps`, [TO_HANDLERS]: `toHandlers`, - [CAMELIZE]: `camelize` + [CAMELIZE]: `camelize`, + [SET_BLOCK_TRACKING]: `setBlockTracking` } export function registerRuntimeHelpers(helpers: any) { diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts index fe717905c..f00656e81 100644 --- a/packages/compiler-core/src/transform.ts +++ b/packages/compiler-core/src/transform.ts @@ -100,7 +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 + cache(exp: T, isVNode?: boolean): CacheExpression | T } function createTransformContext( @@ -219,8 +219,8 @@ function createTransformContext( true ) }, - cache(exp) { - return cacheHandlers ? createCacheExpression(++context.cached, exp) : exp + cache(exp, isVNode = false) { + return createCacheExpression(++context.cached, exp, isVNode) } } @@ -260,12 +260,17 @@ function finalizeRoot(root: RootNode, context: TransformContext) { const codegenNode = child.codegenNode as | ElementCodegenNode | ComponentCodegenNode - if (codegenNode.callee === WITH_DIRECTIVES) { - codegenNode.arguments[0].callee = helper(CREATE_BLOCK) + | CacheExpression + if (codegenNode.type !== NodeTypes.JS_CACHE_EXPRESSION) { + if (codegenNode.callee === WITH_DIRECTIVES) { + codegenNode.arguments[0].callee = helper(CREATE_BLOCK) + } else { + codegenNode.callee = helper(CREATE_BLOCK) + } + root.codegenNode = createBlockExpression(codegenNode, context) } else { - codegenNode.callee = helper(CREATE_BLOCK) + root.codegenNode = codegenNode } - root.codegenNode = createBlockExpression(codegenNode, context) } else { // - single , IfNode, ForNode: already blocks. // - single text node: always patched. diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 9ef9dda2a..7dd38b465 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -4,12 +4,12 @@ import { TemplateChildNode, SimpleExpressionNode, ElementTypes, - ElementCodegenNode, PlainElementNode, ComponentNode, TemplateNode, ElementNode, - PlainElementCodegenNode + PlainElementCodegenNode, + CodegenNodeWithDirective } from '../ast' import { TransformContext } from '../transform' import { WITH_DIRECTIVES } from '../runtimeHelpers' @@ -57,17 +57,20 @@ function walk( } else { // node may contain dynamic children, but its props may be eligible for // hoisting. - const flag = getPatchFlag(child) - if ( - (!flag || - flag === PatchFlags.NEED_PATCH || - flag === PatchFlags.TEXT) && - !hasDynamicKeyOrRef(child) && - !hasCachedProps(child) - ) { - const props = getNodeProps(child) - if (props && props !== `null`) { - getVNodeCall(child).arguments[1] = context.hoist(props) + const codegenNode = child.codegenNode! + if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) { + const flag = getPatchFlag(codegenNode) + if ( + (!flag || + flag === PatchFlags.NEED_PATCH || + flag === PatchFlags.TEXT) && + !hasDynamicKeyOrRef(child) && + !hasCachedProps(child) + ) { + const props = getNodeProps(child) + if (props && props !== `null`) { + getVNodeCall(codegenNode).arguments[1] = context.hoist(props) + } } } } @@ -100,7 +103,11 @@ export function isStaticNode( if (cached !== undefined) { return cached } - const flag = getPatchFlag(node) + const codegenNode = node.codegenNode! + if (codegenNode.type !== NodeTypes.JS_CALL_EXPRESSION) { + return false + } + const flag = getPatchFlag(codegenNode) if (!flag && !hasDynamicKeyOrRef(node) && !hasCachedProps(node)) { // element self is static. check its children. for (let i = 0; i < node.children.length; i++) { @@ -165,26 +172,32 @@ function hasCachedProps(node: PlainElementNode): boolean { return false } -function getVNodeCall(node: PlainElementNode) { - let codegenNode = node.codegenNode as ElementCodegenNode - if (codegenNode.callee === WITH_DIRECTIVES) { - codegenNode = codegenNode.arguments[0] +function getNodeProps(node: PlainElementNode) { + const codegenNode = node.codegenNode! + if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) { + return getVNodeArgAt( + codegenNode, + 1 + ) as PlainElementCodegenNode['arguments'][1] } - return codegenNode } +type NonCachedCodegenNode = + | PlainElementCodegenNode + | CodegenNodeWithDirective + function getVNodeArgAt( - node: PlainElementNode, + node: NonCachedCodegenNode, index: number ): PlainElementCodegenNode['arguments'][number] { return getVNodeCall(node).arguments[index] } -function getPatchFlag(node: PlainElementNode): number | undefined { +function getVNodeCall(node: NonCachedCodegenNode) { + return node.callee === WITH_DIRECTIVES ? node.arguments[0] : node +} + +function getPatchFlag(node: NonCachedCodegenNode): 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 646a51ce6..9fba3d82f 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -280,6 +280,11 @@ export function buildProps( continue } + // skip v-once - it is handled by its dedicated transform. + if (name === 'once') { + continue + } + // special case for v-bind and v-on with no argument const isBind = name === 'bind' const isOn = name === 'on' diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts index 2f060a898..6e4f29ce0 100644 --- a/packages/compiler-core/src/transforms/vFor.ts +++ b/packages/compiler-core/src/transforms/vFor.ts @@ -15,7 +15,8 @@ import { createObjectExpression, createObjectProperty, ForCodegenNode, - ElementCodegenNode + ElementCodegenNode, + SlotOutletCodegenNode } from '../ast' import { createCompilerError, ErrorCodes } from '../errors' import { @@ -130,7 +131,7 @@ export const transformFor = createStructuralDirectiveTransform( : null if (slotOutlet) { // or - childBlock = slotOutlet.codegenNode! + childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode if (isTemplate && keyProperty) { // // we need to inject the key to the renderSlot() call. diff --git a/packages/compiler-core/src/transforms/vModel.ts b/packages/compiler-core/src/transforms/vModel.ts index 81c917203..a0b7f1107 100644 --- a/packages/compiler-core/src/transforms/vModel.ts +++ b/packages/compiler-core/src/transforms/vModel.ts @@ -69,6 +69,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => { if ( !__BROWSER__ && context.prefixIdentifiers && + context.cacheHandlers && !hasScopeRef(exp, context.identifiers) ) { props[1].value = context.cache(props[1].value) diff --git a/packages/compiler-core/src/transforms/vOnce.ts b/packages/compiler-core/src/transforms/vOnce.ts index 170a24c3c..f9d46f1a1 100644 --- a/packages/compiler-core/src/transforms/vOnce.ts +++ b/packages/compiler-core/src/transforms/vOnce.ts @@ -1,17 +1,15 @@ -import { - DirectiveTransform, - createObjectProperty, - createSimpleExpression -} from '@vue/compiler-core' +import { NodeTransform } from '../transform' +import { findDir } from '../utils' +import { NodeTypes } from '../ast' +import { SET_BLOCK_TRACKING } from '../runtimeHelpers' -export const transformOnce: DirectiveTransform = dir => { - return { - props: [ - createObjectProperty( - createSimpleExpression(`$once`, true, dir.loc), - createSimpleExpression('true', false) - ) - ], - needRuntime: false +export const transformOnce: NodeTransform = (node, context) => { + if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) { + context.helper(SET_BLOCK_TRACKING) + return () => { + if (node.codegenNode) { + node.codegenNode = context.cache(node.codegenNode, true /* isVNode */) + } + } } } diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 909d04ce8..b0debfeca 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -86,7 +86,7 @@ export interface ComponentInternalInstance { accessCache: Data | null // cache for render function values that rely on _ctx but won't need updates // after initialized (e.g. inline handlers) - renderCache: Function[] | null + renderCache: (Function | VNode)[] | null components: Record directives: Record diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 16ca8e8da..5508c38fb 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -179,14 +179,10 @@ export function createRenderer< optimized: boolean = false ) { // patching & not same type, unmount old tree - if (n1 != null) { - if (!isSameType(n1, n2)) { - anchor = getNextHostNode(n1) - unmount(n1, parentComponent, parentSuspense, true) - n1 = null - } else if (n1.props && n1.props.$once) { - return - } + if (n1 != null && !isSameType(n1, n2)) { + anchor = getNextHostNode(n1) + unmount(n1, parentComponent, parentSuspense, true) + n1 = null } const { type, shapeFlag } = n2 diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index d55cc7b14..ceb4c6b29 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -49,6 +49,7 @@ export { toString } from './helpers/toString' export { toHandlers } from './helpers/toHandlers' export { renderSlot } from './helpers/renderSlot' export { createSlots } from './helpers/createSlots' +export { setBlockTracking } from './vnode' export { capitalize, camelize } from '@vue/shared' // Internal, for integration with runtime compiler diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 632b160de..2f167b647 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -105,7 +105,24 @@ export function openBlock(disableTracking?: boolean) { blockStack.push((currentBlock = disableTracking ? null : [])) } -let shouldTrack = true +// Whether we should be tracking dynamic child nodes inside a block. +// Only tracks when this value is > 0 +// We are not using a simple boolean because this value may need to be +// incremented/decremented by nested usage of v-once (see below) +let shouldTrack = 1 + +// Block tracking sometimes needs to be disabled, for example during the +// creation of a tree that needs to be cached by v-once. The compiler generates +// code like this: +// _cache[1] || ( +// setBlockTracking(-1), +// _cache[1] = createVNode(...), +// setBlockTracking(1), +// _cache[1] +// ) +export function setBlockTracking(value: number) { + shouldTrack += value +} // Create a block root vnode. Takes the same exact arguments as `createVNode`. // A block root keeps track of dynamic nodes within the block in the @@ -118,9 +135,9 @@ export function createBlock( dynamicProps?: string[] ): VNode { // avoid a block with patchFlag tracking itself - shouldTrack = false + shouldTrack-- const vnode = createVNode(type, props, children, patchFlag, dynamicProps) - shouldTrack = true + shouldTrack++ // save current block children on the block vnode vnode.dynamicChildren = currentBlock || EMPTY_ARR // close block @@ -200,7 +217,7 @@ export function createVNode( // component doesn't need to update, it needs to persist the instance on to // the next vnode so that it can be properly unmounted later. if ( - shouldTrack && + shouldTrack > 0 && currentBlock !== null && (patchFlag > 0 || shapeFlag & ShapeFlags.STATEFUL_COMPONENT || diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 32836f53a..5c7cc6a2e 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -52,7 +52,7 @@ export const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]' export const isReservedProp = (key: string): boolean => - key === 'key' || key === 'ref' || key === '$once' || key.startsWith(`onVnode`) + key === 'key' || key === 'ref' || key.startsWith(`onVnode`) const camelizeRE = /-(\w)/g export const camelize = (str: string): string => {