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)))
})
])
]))
--- /dev/null
+// 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)
+ ]))
+}"
+`;
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]
)
}
}"
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]
)
]))
}
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]
)
]))
}
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]
)
]))
}
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]
)
]))
}
--- /dev/null
+import { baseCompile } from '../../src'
+
+describe('compiler: v-memo transform', () => {
+ function compile(content: string) {
+ return baseCompile(`<div>${content}</div>`, {
+ mode: 'module',
+ prefixIdentifiers: true
+ }).code
+ }
+
+ test('on root element', () => {
+ expect(
+ baseCompile(`<div v-memo="[x]"></div>`, {
+ mode: 'module',
+ prefixIdentifiers: true
+ }).code
+ ).toMatchSnapshot()
+ })
+
+ test('on normal element', () => {
+ expect(compile(`<div v-memo="[x]"></div>`)).toMatchSnapshot()
+ })
+
+ test('on component', () => {
+ expect(compile(`<Comp v-memo="[x]"></Comp>`)).toMatchSnapshot()
+ })
+
+ test('on v-if', () => {
+ expect(
+ compile(
+ `<div v-if="ok" v-memo="[x]"><span>foo</span>bar</div>
+ <Comp v-else v-memo="[x]"></Comp>`
+ )
+ ).toMatchSnapshot()
+ })
+
+ test('on v-for', () => {
+ expect(
+ compile(
+ `<div v-for="{ x, y } in list" :key="x" v-memo="[x, y === z]">
+ <span>foobar</span>
+ </div>`
+ )
+ ).toMatchSnapshot()
+ })
+
+ test('on template v-for', () => {
+ expect(
+ compile(
+ `<template v-for="{ x, y } in list" :key="x" v-memo="[x, y === z]">
+ <span>foobar</span>
+ </template>`
+ )
+ ).toMatchSnapshot()
+ })
+})
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.SIMPLE_EXPRESSION,
content: `() => {}`
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
(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` }, `()`]
(vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.COMPOUND_EXPRESSION,
children: [
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"`
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"`
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`
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
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"`
RENDER_LIST,
OPEN_BLOCK,
FRAGMENT,
- WITH_DIRECTIVES
+ WITH_DIRECTIVES,
+ WITH_MEMO
} from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform'
| VNodeCall
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
+ | MemoExpression // when cached by v-memo
| undefined
ssrCodegenNode?: TemplateLiteral
}
codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
+ | MemoExpression // when cached by v-memo
| undefined
ssrCodegenNode?: CallExpression
}
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 =
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 {
export function createSimpleExpression(
content: SimpleExpressionNode['content'],
- isStatic: SimpleExpressionNode['isStatic'],
+ isStatic: SimpleExpressionNode['isStatic'] = false,
loc: SourceLocation = locStub,
constType: ConstantTypes = ConstantTypes.NOT_CONSTANT
): SimpleExpressionNode {
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
-
- // SSR only types
case NodeTypes.JS_BLOCK_STATEMENT:
- !__BROWSER__ && genNodeList(node.body, context, true, false)
+ genNodeList(node.body, context, true, false)
break
+
+ // SSR only types
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break
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[],
[
transformOnce,
transformIf,
+ transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers
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!
[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) {
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'
}
},
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}`,
return identifier
},
cache(exp, isVNode = false) {
- return createCacheExpression(++context.cached, exp, isVNode)
+ return createCacheExpression(context.cached++, exp, isVNode)
}
}
}
function createRootCodegen(root: RootNode, context: TransformContext) {
- const { helper, removeHelper } = context
+ const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
// 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 {
}
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 <component>
ForRenderListExpression,
BlockCodegenNode,
ForIteratorExpression,
- ConstantTypes
+ ConstantTypes,
+ createBlockStatement,
+ createCompoundExpression
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
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'
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
}
}
- 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)
+ }
}
})
}
)
}
-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)
+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 params
+ return args
+ .slice(0, i + 1)
+ .map((arg, i) => arg || createSimpleExpression(`_`.repeat(i + 1), false))
}
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)$/,
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
-): IfConditionalExpression | BlockCodegenNode {
+): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) {
return createConditionalExpression(
branch.condition,
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
-): BlockCodegenNode {
- const { helper, removeHelper } = context
+): BlockCodegenNode | MemoExpression {
+ const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(
)
}
} 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
}
}
--- /dev/null
+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
+ }
+ }
+ }
+}
TextNode,
InterpolationNode,
VNodeCall,
- SimpleExpressionNode
+ SimpleExpressionNode,
+ BlockCodegenNode,
+ MemoExpression
} from './ast'
import { TransformContext } from './transform'
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'
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))
+ }
+}
},
value: {
type: NodeTypes.JS_CACHE_EXPRESSION,
- index: 1,
+ index: 0,
value: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: V_ON_WITH_KEYS
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 */))
}
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 */))
}
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 */))
}
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)]
])
--- /dev/null
+// 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: `<div v-memo="[x]">{{ x }} {{ y }}</div>`,
+ data: () => ({ x: 0, y: 0 })
+ })
+ expect(el.innerHTML).toBe(`<div>0 0</div>`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 0</div>`)
+
+ vm.y++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 0</div>`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>2 1</div>`)
+ })
+
+ test('on component', async () => {
+ const [el, vm] = mount({
+ template: `<Comp v-memo="[x]" :x="x" :y="y"></Comp>`,
+ data: () => ({ x: 0, y: 0 }),
+ components: {
+ Comp: {
+ props: ['x', 'y'],
+ template: `<div>{{x}} {{y}}</div>`
+ }
+ }
+ })
+ expect(el.innerHTML).toBe(`<div>0 0</div>`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 0</div>`)
+
+ vm.y++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 0</div>`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>2 1</div>`)
+ })
+
+ test('on v-if', async () => {
+ const [el, vm] = mount({
+ template: `<div v-if="ok" v-memo="[x]">{{ x }} {{ y }}</div>
+ <div v-else v-memo="[y]">{{ y }} {{ x }}</div>`,
+ data: () => ({ ok: true, x: 0, y: 0 })
+ })
+ expect(el.innerHTML).toBe(`<div>0 0</div>`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 0</div>`)
+
+ vm.y++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 0</div>`)
+
+ vm.x++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>2 1</div>`)
+
+ vm.ok = false
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>1 2</div>`)
+
+ vm.y++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>2 2</div>`)
+
+ vm.x++
+ // should not update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>2 2</div>`)
+
+ vm.y++
+ // should update
+ await nextTick()
+ expect(el.innerHTML).toBe(`<div>3 3</div>`)
+ })
+
+ test('on v-for', async () => {
+ const [el, vm] = mount({
+ template:
+ `<div v-for="{ x } in list" :key="x" v-memo="[x, x === y]">` +
+ `{{ x }} {{ x === y ? 'yes' : 'no' }} {{ z }}` +
+ `</div>`,
+ data: () => ({
+ list: [{ x: 1 }, { x: 2 }, { x: 3 }],
+ y: 1,
+ z: 'z'
+ })
+ })
+ expect(el.innerHTML).toBe(
+ `<div>1 yes z</div><div>2 no z</div><div>3 no z</div>`
+ )
+
+ vm.y = 2
+ await nextTick()
+ expect(el.innerHTML).toBe(
+ `<div>1 no z</div><div>2 yes z</div><div>3 no z</div>`
+ )
+
+ vm.list[0].x = 4
+ await nextTick()
+ expect(el.innerHTML).toBe(
+ `<div>4 no z</div><div>2 yes z</div><div>3 no z</div>`
+ )
+
+ vm.list[0].x = 5
+ vm.y = 5
+ await nextTick()
+ expect(el.innerHTML).toBe(
+ `<div>5 yes z</div><div>2 no z</div><div>3 no z</div>`
+ )
+
+ vm.z = 'zz'
+ await nextTick()
+ // should not update
+ expect(el.innerHTML).toBe(
+ `<div>5 yes z</div><div>2 no z</div><div>3 no z</div>`
+ )
+ })
+})
-import { VNodeChild } from '../vnode'
+import { VNode, VNodeChild } from '../vnode'
import { isArray, isString, isObject } from '@vue/shared'
import { warn } from '../warning'
*/
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)) {
}
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
- ret = Array.from(source as Iterable<any>, renderItem)
+ ret = Array.from(source as Iterable<any>, (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
}
--- /dev/null
+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
+}
export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot'
export { createSlots } from './helpers/createSlots'
+export { withMemo, isMemoSame } from './helpers/withMemo'
export {
openBlock,
createBlock,
// 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
// 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
// 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) {