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 4ee25d5ac..0a6ca2322 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
@@ -218,7 +218,7 @@ export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"div\\", null, [
_createElementVNode(\\"div\\", null, [
_createElementVNode(\\"div\\", {
- onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.foo && _ctx.foo(...args)))
+ onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.foo && _ctx.foo(...args)))
})
])
]))
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap
new file mode 100644
index 000000000..7e6542862
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap
@@ -0,0 +1,82 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compiler: v-memo transform on component 1`] = `
+"import { resolveComponent as _resolveComponent, createVNode as _createVNode, withMemo as _withMemo, openBlock as _openBlock, createElementBlock as _createElementBlock } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+ const _component_Comp = _resolveComponent(\\"Comp\\")
+
+ return (_openBlock(), _createElementBlock(\\"div\\", null, [
+ _withMemo([_ctx.x], () => _createVNode(_component_Comp), _cache, 0)
+ ]))
+}"
+`;
+
+exports[`compiler: v-memo transform on normal element 1`] = `
+"import { openBlock as _openBlock, createElementBlock as _createElementBlock, withMemo as _withMemo } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock(\\"div\\", null, [
+ _withMemo([_ctx.x], () => (_openBlock(), _createElementBlock(\\"div\\")), _cache, 0)
+ ]))
+}"
+`;
+
+exports[`compiler: v-memo transform on root element 1`] = `
+"import { openBlock as _openBlock, createElementBlock as _createElementBlock, withMemo as _withMemo } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+ return _withMemo([_ctx.x], () => (_openBlock(), _createElementBlock(\\"div\\")), _cache, 0)
+}"
+`;
+
+exports[`compiler: v-memo transform on template v-for 1`] = `
+"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, isMemoSame as _isMemoSame, withMemo as _withMemo } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock(\\"div\\", null, [
+ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
+ const _memo = ([x, y === z])
+ if (_cached && _cached.key === x && _isMemoSame(_cached.memo, _memo)) return _cached
+ const _item = (_openBlock(), _createElementBlock(\\"span\\", { key: x }, \\"foobar\\"))
+ _item.memo = _memo
+ return _item
+ }, _cache, 0), 128 /* KEYED_FRAGMENT */))
+ ]))
+}"
+`;
+
+exports[`compiler: v-memo transform on v-for 1`] = `
+"import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, createElementVNode as _createElementVNode, isMemoSame as _isMemoSame, withMemo as _withMemo } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+ return (_openBlock(), _createElementBlock(\\"div\\", null, [
+ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
+ const _memo = ([x, y === _ctx.z])
+ if (_cached && _cached.key === x && _isMemoSame(_cached.memo, _memo)) return _cached
+ const _item = (_openBlock(), _createElementBlock(\\"div\\", { key: x }, [
+ _createElementVNode(\\"span\\", null, \\"foobar\\")
+ ]))
+ _item.memo = _memo
+ return _item
+ }, _cache, 0), 128 /* KEYED_FRAGMENT */))
+ ]))
+}"
+`;
+
+exports[`compiler: v-memo transform on v-if 1`] = `
+"import { createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, withMemo as _withMemo, createCommentVNode as _createCommentVNode, resolveComponent as _resolveComponent, createBlock as _createBlock } from \\"vue\\"
+
+export function render(_ctx, _cache) {
+ const _component_Comp = _resolveComponent(\\"Comp\\")
+
+ return (_openBlock(), _createElementBlock(\\"div\\", null, [
+ (_ctx.ok)
+ ? _withMemo([_ctx.x], () => (_openBlock(), _createElementBlock(\\"div\\", { key: 0 }, [
+ _createElementVNode(\\"span\\", null, \\"foo\\"),
+ _createTextVNode(\\"bar\\")
+ ])), _cache, 0)
+ : _withMemo([_ctx.x], () => (_openBlock(), _createBlock(_component_Comp, { key: 1 })), _cache, 1)
+ ]))
+}"
+`;
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
index db9e9980f..575c59ebb 100644
--- a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
+++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
@@ -7,11 +7,11 @@ return function render(_ctx, _cache) {
with (_ctx) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue
- return _cache[1] || (
+ return _cache[0] || (
_setBlockTracking(-1),
- _cache[1] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
+ _cache[0] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
_setBlockTracking(1),
- _cache[1]
+ _cache[0]
)
}
}"
@@ -27,11 +27,11 @@ return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createElementBlock(\\"div\\", null, [
- _cache[1] || (
+ _cache[0] || (
_setBlockTracking(-1),
- _cache[1] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
+ _cache[0] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
_setBlockTracking(1),
- _cache[1]
+ _cache[0]
)
]))
}
@@ -46,11 +46,11 @@ return function render(_ctx, _cache) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(\\"div\\", null, [
- _cache[1] || (
+ _cache[0] || (
_setBlockTracking(-1),
- _cache[1] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
+ _cache[0] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
_setBlockTracking(1),
- _cache[1]
+ _cache[0]
)
]))
}
@@ -65,11 +65,11 @@ return function render(_ctx, _cache) {
const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(\\"div\\", null, [
- _cache[1] || (
+ _cache[0] || (
_setBlockTracking(-1),
- _cache[1] = _renderSlot($slots, \\"default\\"),
+ _cache[0] = _renderSlot($slots, \\"default\\"),
_setBlockTracking(1),
- _cache[1]
+ _cache[0]
)
]))
}
@@ -84,11 +84,11 @@ return function render(_ctx, _cache) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(\\"div\\", null, [
- _cache[1] || (
+ _cache[0] || (
_setBlockTracking(-1),
- _cache[1] = _createElementVNode(\\"div\\"),
+ _cache[0] = _createElementVNode(\\"div\\"),
_setBlockTracking(1),
- _cache[1]
+ _cache[0]
)
]))
}
diff --git a/packages/compiler-core/__tests__/transforms/vMemo.spec.ts b/packages/compiler-core/__tests__/transforms/vMemo.spec.ts
new file mode 100644
index 000000000..1b259f7ca
--- /dev/null
+++ b/packages/compiler-core/__tests__/transforms/vMemo.spec.ts
@@ -0,0 +1,56 @@
+import { baseCompile } from '../../src'
+
+describe('compiler: v-memo transform', () => {
+ function compile(content: string) {
+ return baseCompile(`
${content}
`, {
+ mode: 'module',
+ prefixIdentifiers: true
+ }).code
+ }
+
+ test('on root element', () => {
+ expect(
+ baseCompile(``, {
+ mode: 'module',
+ prefixIdentifiers: true
+ }).code
+ ).toMatchSnapshot()
+ })
+
+ test('on normal element', () => {
+ expect(compile(``)).toMatchSnapshot()
+ })
+
+ test('on component', () => {
+ expect(compile(``)).toMatchSnapshot()
+ })
+
+ test('on v-if', () => {
+ expect(
+ compile(
+ `foobar
+ `
+ )
+ ).toMatchSnapshot()
+ })
+
+ test('on v-for', () => {
+ expect(
+ compile(
+ `
+ foobar
+
`
+ )
+ ).toMatchSnapshot()
+ })
+
+ test('on template v-for', () => {
+ expect(
+ compile(
+ `
+ foobar
+ `
+ )
+ ).toMatchSnapshot()
+ })
+})
diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts
index 85b1b93b7..24789d16f 100644
--- a/packages/compiler-core/__tests__/transforms/vOn.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vOn.spec.ts
@@ -452,7 +452,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `() => {}`
@@ -473,7 +473,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
@@ -498,7 +498,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
@@ -543,7 +543,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [`() => `, { content: `_ctx.foo` }, `()`]
@@ -565,7 +565,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
diff --git a/packages/compiler-core/__tests__/transforms/vOnce.spec.ts b/packages/compiler-core/__tests__/transforms/vOnce.spec.ts
index a18a0947d..d3b74f924 100644
--- a/packages/compiler-core/__tests__/transforms/vOnce.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/vOnce.spec.ts
@@ -26,7 +26,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.VNODE_CALL,
tag: `"div"`
@@ -41,7 +41,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.VNODE_CALL,
tag: `"div"`
@@ -56,7 +56,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.VNODE_CALL,
tag: `_component_Comp`
@@ -71,7 +71,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT
@@ -90,7 +90,7 @@ describe('compiler: v-once transform', () => {
expect(root.hoists.length).toBe(0)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.VNODE_CALL,
tag: `"div"`
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index eaf48666b..b087a9845 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -6,7 +6,8 @@ import {
RENDER_LIST,
OPEN_BLOCK,
FRAGMENT,
- WITH_DIRECTIVES
+ WITH_DIRECTIVES,
+ WITH_MEMO
} from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform'
@@ -135,6 +136,7 @@ export interface PlainElementNode extends BaseElementNode {
| VNodeCall
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
+ | MemoExpression // when cached by v-memo
| undefined
ssrCodegenNode?: TemplateLiteral
}
@@ -144,6 +146,7 @@ export interface ComponentNode extends BaseElementNode {
codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
+ | MemoExpression // when cached by v-memo
| undefined
ssrCodegenNode?: CallExpression
}
@@ -375,6 +378,15 @@ export interface CacheExpression extends Node {
isVNode: boolean
}
+export interface MemoExpression extends CallExpression {
+ callee: typeof WITH_MEMO
+ arguments: [ExpressionNode, MemoFactory, string, string]
+}
+
+interface MemoFactory extends FunctionExpression {
+ returns: BlockCodegenNode
+}
+
// SSR-specific Node Types -----------------------------------------------------
export type SSRCodegenNode =
@@ -499,8 +511,8 @@ export interface DynamicSlotFnProperty extends Property {
export type BlockCodegenNode = VNodeCall | RenderSlotCall
export interface IfConditionalExpression extends ConditionalExpression {
- consequent: BlockCodegenNode
- alternate: BlockCodegenNode | IfConditionalExpression
+ consequent: BlockCodegenNode | MemoExpression
+ alternate: BlockCodegenNode | IfConditionalExpression | MemoExpression
}
export interface ForCodegenNode extends VNodeCall {
@@ -627,7 +639,7 @@ export function createObjectProperty(
export function createSimpleExpression(
content: SimpleExpressionNode['content'],
- isStatic: SimpleExpressionNode['isStatic'],
+ isStatic: SimpleExpressionNode['isStatic'] = false,
loc: SourceLocation = locStub,
constType: ConstantTypes = ConstantTypes.NOT_CONSTANT
): SimpleExpressionNode {
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index d930c36cd..eeb13baf8 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -651,11 +651,11 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
+ case NodeTypes.JS_BLOCK_STATEMENT:
+ genNodeList(node.body, context, true, false)
+ break
// SSR only types
- case NodeTypes.JS_BLOCK_STATEMENT:
- !__BROWSER__ && genNodeList(node.body, context, true, false)
- break
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break
diff --git a/packages/compiler-core/src/compile.ts b/packages/compiler-core/src/compile.ts
index 78a6cb62e..fa2dcfbe4 100644
--- a/packages/compiler-core/src/compile.ts
+++ b/packages/compiler-core/src/compile.ts
@@ -17,6 +17,7 @@ import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
+import { transformMemo } from './transforms/vMemo'
export type TransformPreset = [
NodeTransform[],
@@ -30,6 +31,7 @@ export function getBaseTransformPreset(
[
transformOnce,
transformIf,
+ transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts
index d9f7d6c3e..eab675343 100644
--- a/packages/compiler-core/src/runtimeHelpers.ts
+++ b/packages/compiler-core/src/runtimeHelpers.ts
@@ -38,6 +38,8 @@ export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``)
export const IS_REF = Symbol(__DEV__ ? `isRef` : ``)
+export const WITH_MEMO = Symbol(__DEV__ ? `withMemo` : ``)
+export const IS_MEMO_SAME = Symbol(__DEV__ ? `isMemoSame` : ``)
// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
@@ -80,7 +82,9 @@ export const helperNameMap: any = {
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`,
[UNREF]: `unref`,
- [IS_REF]: `isRef`
+ [IS_REF]: `isRef`,
+ [WITH_MEMO]: `withMemo`,
+ [IS_MEMO_SAME]: `isMemoSame`
}
export function registerRuntimeHelpers(helpers: any) {
diff --git a/packages/compiler-core/src/transform.ts b/packages/compiler-core/src/transform.ts
index 44e06fc22..eb328dcd2 100644
--- a/packages/compiler-core/src/transform.ts
+++ b/packages/compiler-core/src/transform.ts
@@ -34,10 +34,9 @@ import {
TO_DISPLAY_STRING,
FRAGMENT,
helperNameMap,
- CREATE_COMMENT,
- OPEN_BLOCK
+ CREATE_COMMENT
} from './runtimeHelpers'
-import { getVNodeBlockHelper, getVNodeHelper, isVSlot } from './utils'
+import { isVSlot, makeBlock } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
import { CompilerCompatOptions } from './compat/compatConfig'
@@ -278,7 +277,7 @@ export function createTransformContext(
}
},
hoist(exp) {
- if (isString(exp)) exp = createSimpleExpression(exp, false)
+ if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
@@ -290,7 +289,7 @@ export function createTransformContext(
return identifier
},
cache(exp, isVNode = false) {
- return createCacheExpression(++context.cached, exp, isVNode)
+ return createCacheExpression(context.cached++, exp, isVNode)
}
}
@@ -337,7 +336,7 @@ export function transform(root: RootNode, options: TransformOptions) {
}
function createRootCodegen(root: RootNode, context: TransformContext) {
- const { helper, removeHelper } = context
+ const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
@@ -347,12 +346,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
// SimpleExpressionNode
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
- if (!codegenNode.isBlock) {
- codegenNode.isBlock = true
- removeHelper(getVNodeHelper(context.inSSR, codegenNode.isComponent))
- helper(OPEN_BLOCK)
- helper(getVNodeBlockHelper(context.inSSR, codegenNode.isComponent))
- }
+ makeBlock(codegenNode, context)
}
root.codegenNode = codegenNode
} else {
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index 0b10b70a2..697e8e294 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -504,8 +504,8 @@ export function buildProps(
}
continue
}
- // skip v-once - it is handled by its dedicated transform.
- if (name === 'once') {
+ // skip v-once/v-memo - they are handled by dedicated transforms.
+ if (name === 'once' || name === 'memo') {
continue
}
// skip v-is and :is on
diff --git a/packages/compiler-core/src/transforms/vFor.ts b/packages/compiler-core/src/transforms/vFor.ts
index a44b724b6..e4b02eaab 100644
--- a/packages/compiler-core/src/transforms/vFor.ts
+++ b/packages/compiler-core/src/transforms/vFor.ts
@@ -24,7 +24,9 @@ import {
ForRenderListExpression,
BlockCodegenNode,
ForIteratorExpression,
- ConstantTypes
+ ConstantTypes,
+ createBlockStatement,
+ createCompoundExpression
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
@@ -34,9 +36,15 @@ import {
isSlotOutlet,
injectProp,
getVNodeBlockHelper,
- getVNodeHelper
+ getVNodeHelper,
+ findDir
} from '../utils'
-import { RENDER_LIST, OPEN_BLOCK, FRAGMENT } from '../runtimeHelpers'
+import {
+ RENDER_LIST,
+ OPEN_BLOCK,
+ FRAGMENT,
+ IS_MEMO_SAME
+} from '../runtimeHelpers'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
@@ -51,15 +59,14 @@ export const transformFor = createStructuralDirectiveTransform(
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source
]) as ForRenderListExpression
+ const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`)
- const keyProperty = keyProp
- ? createObjectProperty(
- `key`,
- keyProp.type === NodeTypes.ATTRIBUTE
- ? createSimpleExpression(keyProp.value!.content, true)
- : keyProp.exp!
- )
- : null
+ const keyExp =
+ keyProp &&
+ (keyProp.type === NodeTypes.ATTRIBUTE
+ ? createSimpleExpression(keyProp.value!.content, true)
+ : keyProp.exp!)
+ const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
if (!__BROWSER__ && context.prefixIdentifiers && keyProperty) {
// #2085 process :key expression needs to be processed in order for it
@@ -189,11 +196,37 @@ export const transformFor = createStructuralDirectiveTransform(
}
}
- renderExp.arguments.push(createFunctionExpression(
- createForLoopParams(forNode.parseResult),
- childBlock,
- true /* force newline */
- ) as ForIteratorExpression)
+ if (memo) {
+ const loop = createFunctionExpression(
+ createForLoopParams(forNode.parseResult, [
+ createSimpleExpression(`_cached`)
+ ])
+ )
+ loop.body = createBlockStatement([
+ createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
+ createCompoundExpression([
+ `if (_cached`,
+ ...(keyExp ? [` && _cached.key === `, keyExp] : []),
+ ` && ${context.helperString(
+ IS_MEMO_SAME
+ )}(_cached.memo, _memo)) return _cached`
+ ]),
+ createCompoundExpression([`const _item = `, childBlock as any]),
+ createSimpleExpression(`_item.memo = _memo`),
+ createSimpleExpression(`return _item`)
+ ])
+ renderExp.arguments.push(
+ loop as ForIteratorExpression,
+ createSimpleExpression(`_cache`),
+ createSimpleExpression(String(context.cached++))
+ )
+ } else {
+ renderExp.arguments.push(createFunctionExpression(
+ createForLoopParams(forNode.parseResult),
+ childBlock,
+ true /* force newline */
+ ) as ForIteratorExpression)
+ }
}
})
}
@@ -393,29 +426,21 @@ function createAliasExpression(
)
}
-export function createForLoopParams({
- value,
- key,
- index
-}: ForParseResult): ExpressionNode[] {
- const params: ExpressionNode[] = []
- if (value) {
- params.push(value)
- }
- if (key) {
- if (!value) {
- params.push(createSimpleExpression(`_`, false))
- }
- params.push(key)
- }
- if (index) {
- if (!key) {
- if (!value) {
- params.push(createSimpleExpression(`_`, false))
- }
- params.push(createSimpleExpression(`__`, false))
- }
- params.push(index)
- }
- return params
+export function createForLoopParams(
+ { value, key, index }: ForParseResult,
+ memoArgs: ExpressionNode[] = []
+): ExpressionNode[] {
+ return createParamsList([value, key, index, ...memoArgs])
+}
+
+function createParamsList(
+ args: (ExpressionNode | undefined)[]
+): ExpressionNode[] {
+ let i = args.length
+ while (i--) {
+ if (args[i]) break
+ }
+ return args
+ .slice(0, i + 1)
+ .map((arg, i) => arg || createSimpleExpression(`_`.repeat(i + 1), false))
}
diff --git a/packages/compiler-core/src/transforms/vIf.ts b/packages/compiler-core/src/transforms/vIf.ts
index 0ce5c540d..01810090c 100644
--- a/packages/compiler-core/src/transforms/vIf.ts
+++ b/packages/compiler-core/src/transforms/vIf.ts
@@ -22,21 +22,22 @@ import {
AttributeNode,
locStub,
CacheExpression,
- ConstantTypes
+ ConstantTypes,
+ MemoExpression
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
-import { FRAGMENT, CREATE_COMMENT, OPEN_BLOCK } from '../runtimeHelpers'
+import { FRAGMENT, CREATE_COMMENT } from '../runtimeHelpers'
import {
injectProp,
findDir,
findProp,
isBuiltInType,
- getVNodeHelper,
- getVNodeBlockHelper
+ makeBlock
} from '../utils'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
+import { getMemoedVNodeCall } from '..'
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
@@ -214,7 +215,7 @@ function createCodegenNodeForBranch(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
-): IfConditionalExpression | BlockCodegenNode {
+): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) {
return createConditionalExpression(
branch.condition,
@@ -235,8 +236,8 @@ function createChildrenCodegenNode(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
-): BlockCodegenNode {
- const { helper, removeHelper } = context
+): BlockCodegenNode | MemoExpression {
+ const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(
@@ -284,18 +285,17 @@ function createChildrenCodegenNode(
)
}
} else {
- const vnodeCall = (firstChild as ElementNode)
- .codegenNode as BlockCodegenNode
+ const ret = (firstChild as ElementNode).codegenNode as
+ | BlockCodegenNode
+ | MemoExpression
+ const vnodeCall = getMemoedVNodeCall(ret)
// Change createVNode to createBlock.
- if (vnodeCall.type === NodeTypes.VNODE_CALL && !vnodeCall.isBlock) {
- removeHelper(getVNodeHelper(context.inSSR, vnodeCall.isComponent))
- vnodeCall.isBlock = true
- helper(OPEN_BLOCK)
- helper(getVNodeBlockHelper(context.inSSR, vnodeCall.isComponent))
+ if (vnodeCall.type === NodeTypes.VNODE_CALL) {
+ makeBlock(vnodeCall, context)
}
// inject branch key
injectProp(vnodeCall, keyProperty, context)
- return vnodeCall
+ return ret
}
}
diff --git a/packages/compiler-core/src/transforms/vMemo.ts b/packages/compiler-core/src/transforms/vMemo.ts
new file mode 100644
index 000000000..4e150875d
--- /dev/null
+++ b/packages/compiler-core/src/transforms/vMemo.ts
@@ -0,0 +1,40 @@
+import { NodeTransform } from '../transform'
+import { findDir, makeBlock } from '../utils'
+import {
+ createCallExpression,
+ createFunctionExpression,
+ ElementTypes,
+ MemoExpression,
+ NodeTypes,
+ PlainElementNode
+} from '../ast'
+import { WITH_MEMO } from '../runtimeHelpers'
+
+const seen = new WeakSet()
+
+export const transformMemo: NodeTransform = (node, context) => {
+ if (node.type === NodeTypes.ELEMENT) {
+ const dir = findDir(node, 'memo')
+ if (!dir || seen.has(node)) {
+ return
+ }
+ seen.add(node)
+ return () => {
+ const codegenNode =
+ node.codegenNode ||
+ (context.currentNode as PlainElementNode).codegenNode
+ if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
+ // non-component sub tree should be turned into a block
+ if (node.tagType !== ElementTypes.COMPONENT) {
+ makeBlock(codegenNode, context)
+ }
+ node.codegenNode = createCallExpression(context.helper(WITH_MEMO), [
+ dir.exp!,
+ createFunctionExpression(undefined, codegenNode),
+ `_cache`,
+ String(context.cached++)
+ ]) as MemoExpression
+ }
+ }
+ }
+}
diff --git a/packages/compiler-core/src/utils.ts b/packages/compiler-core/src/utils.ts
index b0aca02d8..b02379178 100644
--- a/packages/compiler-core/src/utils.ts
+++ b/packages/compiler-core/src/utils.ts
@@ -21,7 +21,9 @@ import {
TextNode,
InterpolationNode,
VNodeCall,
- SimpleExpressionNode
+ SimpleExpressionNode,
+ BlockCodegenNode,
+ MemoExpression
} from './ast'
import { TransformContext } from './transform'
import {
@@ -36,7 +38,9 @@ import {
CREATE_BLOCK,
CREATE_ELEMENT_BLOCK,
CREATE_VNODE,
- CREATE_ELEMENT_VNODE
+ CREATE_ELEMENT_VNODE,
+ WITH_MEMO,
+ OPEN_BLOCK
} from './runtimeHelpers'
import { isString, isObject, hyphenate, extend } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
@@ -483,3 +487,23 @@ export function hasScopeRef(
return false
}
}
+
+export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
+ if (node.type === NodeTypes.JS_CALL_EXPRESSION && node.callee === WITH_MEMO) {
+ return node.arguments[1].returns as VNodeCall
+ } else {
+ return node
+ }
+}
+
+export function makeBlock(
+ node: VNodeCall,
+ { helper, removeHelper, inSSR }: TransformContext
+) {
+ if (!node.isBlock) {
+ node.isBlock = true
+ removeHelper(getVNodeHelper(inSSR, node.isComponent))
+ helper(OPEN_BLOCK)
+ helper(getVNodeBlockHelper(inSSR, node.isComponent))
+ }
+}
diff --git a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
index 84896a60d..b148422b8 100644
--- a/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/vOn.spec.ts
@@ -278,7 +278,7 @@ describe('compiler-dom: transform v-on', () => {
},
value: {
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: V_ON_WITH_KEYS
diff --git a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
index ecf1e4696..d712b9df3 100644
--- a/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
@@ -414,19 +414,19 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"div\\", {
- onClick: _cache[1] || (_cache[1] = $event => (count.value = 1))
+ onClick: _cache[0] || (_cache[0] = $event => (count.value = 1))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[2] || (_cache[2] = $event => (maybe.value = count.value))
+ onClick: _cache[1] || (_cache[1] = $event => (maybe.value = count.value))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = count.value : lett = count.value))
+ onClick: _cache[2] || (_cache[2] = $event => (_isRef(lett) ? lett.value = count.value : lett = count.value))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[4] || (_cache[4] = $event => (_isRef(v) ? v.value += 1 : v += 1))
+ onClick: _cache[3] || (_cache[3] = $event => (_isRef(v) ? v.value += 1 : v += 1))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[5] || (_cache[5] = $event => (_isRef(v) ? v.value -= 1 : v -= 1))
+ onClick: _cache[4] || (_cache[4] = $event => (_isRef(v) ? v.value -= 1 : v -= 1))
})
], 64 /* STABLE_FRAGMENT */))
}
@@ -451,13 +451,13 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"div\\", {
- onClick: _cache[1] || (_cache[1] = $event => (({ count: count.value } = val)))
+ onClick: _cache[0] || (_cache[0] = $event => (({ count: count.value } = val)))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[2] || (_cache[2] = $event => ([maybe.value] = val))
+ onClick: _cache[1] || (_cache[1] = $event => ([maybe.value] = val))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[3] || (_cache[3] = $event => (({ lett: lett } = val)))
+ onClick: _cache[2] || (_cache[2] = $event => (({ lett: lett } = val)))
})
], 64 /* STABLE_FRAGMENT */))
}
@@ -481,22 +481,22 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"div\\", {
- onClick: _cache[1] || (_cache[1] = $event => (count.value++))
+ onClick: _cache[0] || (_cache[0] = $event => (count.value++))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[2] || (_cache[2] = $event => (--count.value))
+ onClick: _cache[1] || (_cache[1] = $event => (--count.value))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[3] || (_cache[3] = $event => (maybe.value++))
+ onClick: _cache[2] || (_cache[2] = $event => (maybe.value++))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[4] || (_cache[4] = $event => (--maybe.value))
+ onClick: _cache[3] || (_cache[3] = $event => (--maybe.value))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? lett.value++ : lett++))
+ onClick: _cache[4] || (_cache[4] = $event => (_isRef(lett) ? lett.value++ : lett++))
}),
_createElementVNode(\\"div\\", {
- onClick: _cache[6] || (_cache[6] = $event => (_isRef(lett) ? --lett.value : --lett))
+ onClick: _cache[5] || (_cache[5] = $event => (_isRef(lett) ? --lett.value : --lett))
})
], 64 /* STABLE_FRAGMENT */))
}
@@ -520,17 +520,17 @@ export default {
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_withDirectives(_createElementVNode(\\"input\\", {
- \\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (count.value = $event))
+ \\"onUpdate:modelValue\\": _cache[0] || (_cache[0] = $event => (count.value = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, count.value]
]),
_withDirectives(_createElementVNode(\\"input\\", {
- \\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(maybe) ? maybe.value = $event : null))
+ \\"onUpdate:modelValue\\": _cache[1] || (_cache[1] = $event => (_isRef(maybe) ? maybe.value = $event : null))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(maybe)]
]),
_withDirectives(_createElementVNode(\\"input\\", {
- \\"onUpdate:modelValue\\": _cache[3] || (_cache[3] = $event => (_isRef(lett) ? lett.value = $event : lett = $event))
+ \\"onUpdate:modelValue\\": _cache[2] || (_cache[2] = $event => (_isRef(lett) ? lett.value = $event : lett = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(lett)]
])
diff --git a/packages/runtime-core/__tests__/helpers/withMemo.spec.ts b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts
new file mode 100644
index 000000000..b98b8c293
--- /dev/null
+++ b/packages/runtime-core/__tests__/helpers/withMemo.spec.ts
@@ -0,0 +1,150 @@
+// since v-memo really is a compiler + runtime combo feature, we are performing
+// more of an itegration test here.
+import { ComponentOptions, createApp, nextTick } from 'vue'
+
+describe('v-memo', () => {
+ function mount(options: ComponentOptions): [HTMLElement, any] {
+ const app = createApp(options)
+ const el = document.createElement('div')
+ const vm = app.mount(el)
+ return [el, vm]
+ }
+
+ test('on normal element', async () => {
+ const [el, vm] = mount({
+ template: `{{ x }} {{ y }}
`,
+ data: () => ({ x: 0, y: 0 })
+ })
+ expect(el.innerHTML).toBe(`0 0
`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 0
`)
+
+ vm.y++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 0
`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`2 1
`)
+ })
+
+ test('on component', async () => {
+ const [el, vm] = mount({
+ template: ``,
+ data: () => ({ x: 0, y: 0 }),
+ components: {
+ Comp: {
+ props: ['x', 'y'],
+ template: `{{x}} {{y}}
`
+ }
+ }
+ })
+ expect(el.innerHTML).toBe(`0 0
`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 0
`)
+
+ vm.y++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 0
`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`2 1
`)
+ })
+
+ test('on v-if', async () => {
+ const [el, vm] = mount({
+ template: `{{ x }} {{ y }}
+ {{ y }} {{ x }}
`,
+ data: () => ({ ok: true, x: 0, y: 0 })
+ })
+ expect(el.innerHTML).toBe(`0 0
`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 0
`)
+
+ vm.y++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 0
`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`2 1
`)
+
+ vm.ok = false
+ await nextTick()
+ expect(el.innerHTML).toBe(`1 2
`)
+
+ vm.y++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`2 2
`)
+
+ vm.x++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`2 2
`)
+
+ vm.y++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`3 3
`)
+ })
+
+ test('on v-for', async () => {
+ const [el, vm] = mount({
+ template:
+ `` +
+ `{{ x }} {{ x === y ? 'yes' : 'no' }} {{ z }}` +
+ `
`,
+ data: () => ({
+ list: [{ x: 1 }, { x: 2 }, { x: 3 }],
+ y: 1,
+ z: 'z'
+ })
+ })
+ expect(el.innerHTML).toBe(
+ `1 yes z
2 no z
3 no z
`
+ )
+
+ vm.y = 2
+ await nextTick()
+ expect(el.innerHTML).toBe(
+ `1 no z
2 yes z
3 no z
`
+ )
+
+ vm.list[0].x = 4
+ await nextTick()
+ expect(el.innerHTML).toBe(
+ `4 no z
2 yes z
3 no z
`
+ )
+
+ vm.list[0].x = 5
+ vm.y = 5
+ await nextTick()
+ expect(el.innerHTML).toBe(
+ `5 yes z
2 no z
3 no z
`
+ )
+
+ vm.z = 'zz'
+ await nextTick()
+ // should not update
+ expect(el.innerHTML).toBe(
+ `5 yes z
2 no z
3 no z
`
+ )
+ })
+})
diff --git a/packages/runtime-core/src/helpers/renderList.ts b/packages/runtime-core/src/helpers/renderList.ts
index de4ab8afa..543c343dd 100644
--- a/packages/runtime-core/src/helpers/renderList.ts
+++ b/packages/runtime-core/src/helpers/renderList.ts
@@ -1,4 +1,4 @@
-import { VNodeChild } from '../vnode'
+import { VNode, VNodeChild } from '../vnode'
import { isArray, isString, isObject } from '@vue/shared'
import { warn } from '../warning'
@@ -52,13 +52,17 @@ export function renderList(
*/
export function renderList(
source: any,
- renderItem: (...args: any[]) => VNodeChild
+ renderItem: (...args: any[]) => VNodeChild,
+ cache?: any[],
+ index?: number
): VNodeChild[] {
let ret: VNodeChild[]
+ const cached = (cache && cache[index!]) as VNode[] | undefined
+
if (isArray(source) || isString(source)) {
ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) {
- ret[i] = renderItem(source[i], i)
+ ret[i] = renderItem(source[i], i, undefined, cached && cached[i])
}
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
@@ -71,17 +75,23 @@ export function renderList(
}
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
- ret = Array.from(source as Iterable, renderItem)
+ ret = Array.from(source as Iterable, (item, i) =>
+ renderItem(item, i, undefined, cached && cached[i])
+ )
} else {
const keys = Object.keys(source)
ret = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
- ret[i] = renderItem(source[key], key, i)
+ ret[i] = renderItem(source[key], key, i, cached && cached[i])
}
}
} else {
ret = []
}
+
+ if (cache) {
+ cache[index!] = ret
+ }
return ret
}
diff --git a/packages/runtime-core/src/helpers/withMemo.ts b/packages/runtime-core/src/helpers/withMemo.ts
new file mode 100644
index 000000000..33a243c93
--- /dev/null
+++ b/packages/runtime-core/src/helpers/withMemo.ts
@@ -0,0 +1,29 @@
+import { currentBlock, isBlockTreeEnabled, VNode } from '../vnode'
+
+export function withMemo(
+ memo: any[],
+ render: () => VNode,
+ cache: any[],
+ index: number
+) {
+ const cached = cache[index] as VNode | undefined
+ if (cached && isMemoSame(cached.memo!, memo)) {
+ // make sure to let parent block track it when returning cached
+ if (isBlockTreeEnabled > 0 && currentBlock) {
+ currentBlock.push(cached)
+ }
+ return cached
+ }
+ const ret = render()
+ ret.memo = memo
+ return (cache[index] = ret)
+}
+
+export function isMemoSame(prev: any[], next: any[]) {
+ for (let i = 0; i < prev.length; i++) {
+ if (prev[i] !== next[i]) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 3b7457355..5d3efba09 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -264,6 +264,7 @@ export { renderList } from './helpers/renderList'
export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot'
export { createSlots } from './helpers/createSlots'
+export { withMemo, isMemoSame } from './helpers/withMemo'
export {
openBlock,
createBlock,
diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts
index 89d3507aa..aceaf3a90 100644
--- a/packages/runtime-core/src/vnode.ts
+++ b/packages/runtime-core/src/vnode.ts
@@ -182,6 +182,9 @@ export interface VNode<
// application root node only
appContext: AppContext | null
+
+ // v-for memo
+ memo?: any[]
}
// Since v-if and v-for are the two possible ways node structure can dynamically
@@ -221,7 +224,7 @@ export function closeBlock() {
// 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 isBlockTreeEnabled = 1
+export let isBlockTreeEnabled = 1
/**
* Block tracking sometimes needs to be disabled, for example during the
@@ -692,7 +695,7 @@ export function normalizeVNode(child: VNodeChild): VNode {
// optimized normalization for template-compiled render fns
export function cloneIfMounted(child: VNode): VNode {
- return child.el === null ? child : cloneVNode(child)
+ return child.el === null || child.memo ? child : cloneVNode(child)
}
export function normalizeChildren(vnode: VNode, children: unknown) {