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 => {