feat: v-memo

This commit is contained in:
Evan You 2021-07-09 21:41:44 -04:00
parent 5cea9a1d4e
commit 3b64508e3b
23 changed files with 563 additions and 131 deletions

View File

@ -218,7 +218,7 @@ export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_createElementVNode(\\"div\\", null, [ _createElementVNode(\\"div\\", null, [
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.foo && _ctx.foo(...args))) onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.foo && _ctx.foo(...args)))
}) })
]) ])
])) ]))

View File

@ -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)
]))
}"
`;

View File

@ -7,11 +7,11 @@ return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode } = _Vue
return _cache[1] || ( return _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1),
_cache[1] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]), _cache[0] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
_setBlockTracking(1), _setBlockTracking(1),
_cache[1] _cache[0]
) )
} }
}" }"
@ -27,11 +27,11 @@ return function render(_ctx, _cache) {
const _component_Comp = _resolveComponent(\\"Comp\\") const _component_Comp = _resolveComponent(\\"Comp\\")
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_cache[1] || ( _cache[0] || (
_setBlockTracking(-1), _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), _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 const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_cache[1] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1),
_cache[1] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]), _cache[0] = _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
_setBlockTracking(1), _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 const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_cache[1] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1),
_cache[1] = _renderSlot($slots, \\"default\\"), _cache[0] = _renderSlot($slots, \\"default\\"),
_setBlockTracking(1), _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 const { setBlockTracking: _setBlockTracking, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock(\\"div\\", null, [ return (_openBlock(), _createElementBlock(\\"div\\", null, [
_cache[1] || ( _cache[0] || (
_setBlockTracking(-1), _setBlockTracking(-1),
_cache[1] = _createElementVNode(\\"div\\"), _cache[0] = _createElementVNode(\\"div\\"),
_setBlockTracking(1), _setBlockTracking(1),
_cache[1] _cache[0]
) )
])) ]))
} }

View File

@ -0,0 +1,56 @@
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()
})
})

View File

@ -452,7 +452,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value (vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({ ).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.SIMPLE_EXPRESSION,
content: `() => {}` content: `() => {}`
@ -473,7 +473,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value (vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({ ).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
@ -498,7 +498,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value (vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({ ).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [
@ -543,7 +543,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value (vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({ ).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [`() => `, { content: `_ctx.foo` }, `()`] children: [`() => `, { content: `_ctx.foo` }, `()`]
@ -565,7 +565,7 @@ describe('compiler: transform v-on', () => {
(vnodeCall.props as ObjectExpression).properties[0].value (vnodeCall.props as ObjectExpression).properties[0].value
).toMatchObject({ ).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.COMPOUND_EXPRESSION, type: NodeTypes.COMPOUND_EXPRESSION,
children: [ children: [

View File

@ -26,7 +26,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.VNODE_CALL, type: NodeTypes.VNODE_CALL,
tag: `"div"` tag: `"div"`
@ -41,7 +41,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.VNODE_CALL, type: NodeTypes.VNODE_CALL,
tag: `"div"` tag: `"div"`
@ -56,7 +56,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.VNODE_CALL, type: NodeTypes.VNODE_CALL,
tag: `_component_Comp` tag: `_component_Comp`
@ -71,7 +71,7 @@ describe('compiler: v-once transform', () => {
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_SLOT callee: RENDER_SLOT
@ -90,7 +90,7 @@ describe('compiler: v-once transform', () => {
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.VNODE_CALL, type: NodeTypes.VNODE_CALL,
tag: `"div"` tag: `"div"`

View File

@ -6,7 +6,8 @@ import {
RENDER_LIST, RENDER_LIST,
OPEN_BLOCK, OPEN_BLOCK,
FRAGMENT, FRAGMENT,
WITH_DIRECTIVES WITH_DIRECTIVES,
WITH_MEMO
} from './runtimeHelpers' } from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement' import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform' import { ImportItem, TransformContext } from './transform'
@ -135,6 +136,7 @@ export interface PlainElementNode extends BaseElementNode {
| VNodeCall | VNodeCall
| SimpleExpressionNode // when hoisted | SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once | CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
| undefined | undefined
ssrCodegenNode?: TemplateLiteral ssrCodegenNode?: TemplateLiteral
} }
@ -144,6 +146,7 @@ export interface ComponentNode extends BaseElementNode {
codegenNode: codegenNode:
| VNodeCall | VNodeCall
| CacheExpression // when cached by v-once | CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
| undefined | undefined
ssrCodegenNode?: CallExpression ssrCodegenNode?: CallExpression
} }
@ -375,6 +378,15 @@ export interface CacheExpression extends Node {
isVNode: boolean 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 ----------------------------------------------------- // SSR-specific Node Types -----------------------------------------------------
export type SSRCodegenNode = export type SSRCodegenNode =
@ -499,8 +511,8 @@ export interface DynamicSlotFnProperty extends Property {
export type BlockCodegenNode = VNodeCall | RenderSlotCall export type BlockCodegenNode = VNodeCall | RenderSlotCall
export interface IfConditionalExpression extends ConditionalExpression { export interface IfConditionalExpression extends ConditionalExpression {
consequent: BlockCodegenNode consequent: BlockCodegenNode | MemoExpression
alternate: BlockCodegenNode | IfConditionalExpression alternate: BlockCodegenNode | IfConditionalExpression | MemoExpression
} }
export interface ForCodegenNode extends VNodeCall { export interface ForCodegenNode extends VNodeCall {
@ -627,7 +639,7 @@ export function createObjectProperty(
export function createSimpleExpression( export function createSimpleExpression(
content: SimpleExpressionNode['content'], content: SimpleExpressionNode['content'],
isStatic: SimpleExpressionNode['isStatic'], isStatic: SimpleExpressionNode['isStatic'] = false,
loc: SourceLocation = locStub, loc: SourceLocation = locStub,
constType: ConstantTypes = ConstantTypes.NOT_CONSTANT constType: ConstantTypes = ConstantTypes.NOT_CONSTANT
): SimpleExpressionNode { ): SimpleExpressionNode {

View File

@ -651,11 +651,11 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_CACHE_EXPRESSION: case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context) genCacheExpression(node, context)
break break
case NodeTypes.JS_BLOCK_STATEMENT:
genNodeList(node.body, context, true, false)
break
// SSR only types // SSR only types
case NodeTypes.JS_BLOCK_STATEMENT:
!__BROWSER__ && genNodeList(node.body, context, true, false)
break
case NodeTypes.JS_TEMPLATE_LITERAL: case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context) !__BROWSER__ && genTemplateLiteral(node, context)
break break

View File

@ -17,6 +17,7 @@ import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel' import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter' import { transformFilter } from './compat/transformFilter'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors' import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { transformMemo } from './transforms/vMemo'
export type TransformPreset = [ export type TransformPreset = [
NodeTransform[], NodeTransform[],
@ -30,6 +31,7 @@ export function getBaseTransformPreset(
[ [
transformOnce, transformOnce,
transformIf, transformIf,
transformMemo,
transformFor, transformFor,
...(__COMPAT__ ? [transformFilter] : []), ...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers ...(!__BROWSER__ && prefixIdentifiers

View File

@ -38,6 +38,8 @@ export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``) export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``) export const UNREF = Symbol(__DEV__ ? `unref` : ``)
export const IS_REF = Symbol(__DEV__ ? `isRef` : ``) 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 // Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime! // generated code. Make sure these are correctly exported in the runtime!
@ -80,7 +82,9 @@ export const helperNameMap: any = {
[WITH_SCOPE_ID]: `withScopeId`, [WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`, [WITH_CTX]: `withCtx`,
[UNREF]: `unref`, [UNREF]: `unref`,
[IS_REF]: `isRef` [IS_REF]: `isRef`,
[WITH_MEMO]: `withMemo`,
[IS_MEMO_SAME]: `isMemoSame`
} }
export function registerRuntimeHelpers(helpers: any) { export function registerRuntimeHelpers(helpers: any) {

View File

@ -34,10 +34,9 @@ import {
TO_DISPLAY_STRING, TO_DISPLAY_STRING,
FRAGMENT, FRAGMENT,
helperNameMap, helperNameMap,
CREATE_COMMENT, CREATE_COMMENT
OPEN_BLOCK
} from './runtimeHelpers' } from './runtimeHelpers'
import { getVNodeBlockHelper, getVNodeHelper, isVSlot } from './utils' import { isVSlot, makeBlock } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic' import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
import { CompilerCompatOptions } from './compat/compatConfig' import { CompilerCompatOptions } from './compat/compatConfig'
@ -278,7 +277,7 @@ export function createTransformContext(
} }
}, },
hoist(exp) { hoist(exp) {
if (isString(exp)) exp = createSimpleExpression(exp, false) if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp) context.hoists.push(exp)
const identifier = createSimpleExpression( const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`, `_hoisted_${context.hoists.length}`,
@ -290,7 +289,7 @@ export function createTransformContext(
return identifier return identifier
}, },
cache(exp, isVNode = false) { 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) { function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper, removeHelper } = context const { helper } = context
const { children } = root const { children } = root
if (children.length === 1) { if (children.length === 1) {
const child = children[0] const child = children[0]
@ -347,12 +346,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
// SimpleExpressionNode // SimpleExpressionNode
const codegenNode = child.codegenNode const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) { if (codegenNode.type === NodeTypes.VNODE_CALL) {
if (!codegenNode.isBlock) { makeBlock(codegenNode, context)
codegenNode.isBlock = true
removeHelper(getVNodeHelper(context.inSSR, codegenNode.isComponent))
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(context.inSSR, codegenNode.isComponent))
}
} }
root.codegenNode = codegenNode root.codegenNode = codegenNode
} else { } else {

View File

@ -504,8 +504,8 @@ export function buildProps(
} }
continue continue
} }
// skip v-once - it is handled by its dedicated transform. // skip v-once/v-memo - they are handled by dedicated transforms.
if (name === 'once') { if (name === 'once' || name === 'memo') {
continue continue
} }
// skip v-is and :is on <component> // skip v-is and :is on <component>

View File

@ -24,7 +24,9 @@ import {
ForRenderListExpression, ForRenderListExpression,
BlockCodegenNode, BlockCodegenNode,
ForIteratorExpression, ForIteratorExpression,
ConstantTypes ConstantTypes,
createBlockStatement,
createCompoundExpression
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
@ -34,9 +36,15 @@ import {
isSlotOutlet, isSlotOutlet,
injectProp, injectProp,
getVNodeBlockHelper, getVNodeBlockHelper,
getVNodeHelper getVNodeHelper,
findDir
} from '../utils' } 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 { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared' import { PatchFlags, PatchFlagNames } from '@vue/shared'
@ -51,15 +59,14 @@ export const transformFor = createStructuralDirectiveTransform(
const renderExp = createCallExpression(helper(RENDER_LIST), [ const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source forNode.source
]) as ForRenderListExpression ]) as ForRenderListExpression
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`) const keyProp = findProp(node, `key`)
const keyProperty = keyProp const keyExp =
? createObjectProperty( keyProp &&
`key`, (keyProp.type === NodeTypes.ATTRIBUTE
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true) ? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp! : keyProp.exp!)
) const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
: null
if (!__BROWSER__ && context.prefixIdentifiers && keyProperty) { if (!__BROWSER__ && context.prefixIdentifiers && keyProperty) {
// #2085 process :key expression needs to be processed in order for it // #2085 process :key expression needs to be processed in order for it
@ -189,12 +196,38 @@ export const transformFor = createStructuralDirectiveTransform(
} }
} }
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( renderExp.arguments.push(createFunctionExpression(
createForLoopParams(forNode.parseResult), createForLoopParams(forNode.parseResult),
childBlock, childBlock,
true /* force newline */ true /* force newline */
) as ForIteratorExpression) ) as ForIteratorExpression)
} }
}
}) })
} }
) )
@ -393,29 +426,21 @@ function createAliasExpression(
) )
} }
export function createForLoopParams({ export function createForLoopParams(
value, { value, key, index }: ForParseResult,
key, memoArgs: ExpressionNode[] = []
index ): ExpressionNode[] {
}: ForParseResult): ExpressionNode[] { return createParamsList([value, key, index, ...memoArgs])
const params: ExpressionNode[] = []
if (value) {
params.push(value)
} }
if (key) {
if (!value) { function createParamsList(
params.push(createSimpleExpression(`_`, false)) args: (ExpressionNode | undefined)[]
): ExpressionNode[] {
let i = args.length
while (i--) {
if (args[i]) break
} }
params.push(key) return args
} .slice(0, i + 1)
if (index) { .map((arg, i) => arg || createSimpleExpression(`_`.repeat(i + 1), false))
if (!key) {
if (!value) {
params.push(createSimpleExpression(`_`, false))
}
params.push(createSimpleExpression(`__`, false))
}
params.push(index)
}
return params
} }

View File

@ -22,21 +22,22 @@ import {
AttributeNode, AttributeNode,
locStub, locStub,
CacheExpression, CacheExpression,
ConstantTypes ConstantTypes,
MemoExpression
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression' import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression' import { validateBrowserExpression } from '../validateExpression'
import { FRAGMENT, CREATE_COMMENT, OPEN_BLOCK } from '../runtimeHelpers' import { FRAGMENT, CREATE_COMMENT } from '../runtimeHelpers'
import { import {
injectProp, injectProp,
findDir, findDir,
findProp, findProp,
isBuiltInType, isBuiltInType,
getVNodeHelper, makeBlock
getVNodeBlockHelper
} from '../utils' } from '../utils'
import { PatchFlags, PatchFlagNames } from '@vue/shared' import { PatchFlags, PatchFlagNames } from '@vue/shared'
import { getMemoedVNodeCall } from '..'
export const transformIf = createStructuralDirectiveTransform( export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, /^(if|else|else-if)$/,
@ -214,7 +215,7 @@ function createCodegenNodeForBranch(
branch: IfBranchNode, branch: IfBranchNode,
keyIndex: number, keyIndex: number,
context: TransformContext context: TransformContext
): IfConditionalExpression | BlockCodegenNode { ): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) { if (branch.condition) {
return createConditionalExpression( return createConditionalExpression(
branch.condition, branch.condition,
@ -235,8 +236,8 @@ function createChildrenCodegenNode(
branch: IfBranchNode, branch: IfBranchNode,
keyIndex: number, keyIndex: number,
context: TransformContext context: TransformContext
): BlockCodegenNode { ): BlockCodegenNode | MemoExpression {
const { helper, removeHelper } = context const { helper } = context
const keyProperty = createObjectProperty( const keyProperty = createObjectProperty(
`key`, `key`,
createSimpleExpression( createSimpleExpression(
@ -284,18 +285,17 @@ function createChildrenCodegenNode(
) )
} }
} else { } else {
const vnodeCall = (firstChild as ElementNode) const ret = (firstChild as ElementNode).codegenNode as
.codegenNode as BlockCodegenNode | BlockCodegenNode
| MemoExpression
const vnodeCall = getMemoedVNodeCall(ret)
// Change createVNode to createBlock. // Change createVNode to createBlock.
if (vnodeCall.type === NodeTypes.VNODE_CALL && !vnodeCall.isBlock) { if (vnodeCall.type === NodeTypes.VNODE_CALL) {
removeHelper(getVNodeHelper(context.inSSR, vnodeCall.isComponent)) makeBlock(vnodeCall, context)
vnodeCall.isBlock = true
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(context.inSSR, vnodeCall.isComponent))
} }
// inject branch key // inject branch key
injectProp(vnodeCall, keyProperty, context) injectProp(vnodeCall, keyProperty, context)
return vnodeCall return ret
} }
} }

View File

@ -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
}
}
}
}

View File

@ -21,7 +21,9 @@ import {
TextNode, TextNode,
InterpolationNode, InterpolationNode,
VNodeCall, VNodeCall,
SimpleExpressionNode SimpleExpressionNode,
BlockCodegenNode,
MemoExpression
} from './ast' } from './ast'
import { TransformContext } from './transform' import { TransformContext } from './transform'
import { import {
@ -36,7 +38,9 @@ import {
CREATE_BLOCK, CREATE_BLOCK,
CREATE_ELEMENT_BLOCK, CREATE_ELEMENT_BLOCK,
CREATE_VNODE, CREATE_VNODE,
CREATE_ELEMENT_VNODE CREATE_ELEMENT_VNODE,
WITH_MEMO,
OPEN_BLOCK
} from './runtimeHelpers' } from './runtimeHelpers'
import { isString, isObject, hyphenate, extend } from '@vue/shared' import { isString, isObject, hyphenate, extend } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement' import { PropsExpression } from './transforms/transformElement'
@ -483,3 +487,23 @@ export function hasScopeRef(
return false 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))
}
}

View File

@ -278,7 +278,7 @@ describe('compiler-dom: transform v-on', () => {
}, },
value: { value: {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index: 1, index: 0,
value: { value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: V_ON_WITH_KEYS callee: V_ON_WITH_KEYS

View File

@ -414,19 +414,19 @@ export default {
return (_ctx, _cache) => { return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = $event => (count.value = 1)) onClick: _cache[0] || (_cache[0] = $event => (count.value = 1))
}), }),
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[2] || (_cache[2] = $event => (maybe.value = count.value)) onClick: _cache[1] || (_cache[1] = $event => (maybe.value = count.value))
}), }),
_createElementVNode(\\"div\\", { _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\\", { _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\\", { _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 */)) ], 64 /* STABLE_FRAGMENT */))
} }
@ -451,13 +451,13 @@ export default {
return (_ctx, _cache) => { return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = $event => (({ count: count.value } = val))) onClick: _cache[0] || (_cache[0] = $event => (({ count: count.value } = val)))
}), }),
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[2] || (_cache[2] = $event => ([maybe.value] = val)) onClick: _cache[1] || (_cache[1] = $event => ([maybe.value] = val))
}), }),
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[3] || (_cache[3] = $event => (({ lett: lett } = val))) onClick: _cache[2] || (_cache[2] = $event => (({ lett: lett } = val)))
}) })
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
} }
@ -481,22 +481,22 @@ export default {
return (_ctx, _cache) => { return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[1] || (_cache[1] = $event => (count.value++)) onClick: _cache[0] || (_cache[0] = $event => (count.value++))
}), }),
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[2] || (_cache[2] = $event => (--count.value)) onClick: _cache[1] || (_cache[1] = $event => (--count.value))
}), }),
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[3] || (_cache[3] = $event => (maybe.value++)) onClick: _cache[2] || (_cache[2] = $event => (maybe.value++))
}), }),
_createElementVNode(\\"div\\", { _createElementVNode(\\"div\\", {
onClick: _cache[4] || (_cache[4] = $event => (--maybe.value)) onClick: _cache[3] || (_cache[3] = $event => (--maybe.value))
}), }),
_createElementVNode(\\"div\\", { _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\\", { _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 */)) ], 64 /* STABLE_FRAGMENT */))
} }
@ -520,17 +520,17 @@ export default {
return (_ctx, _cache) => { return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_withDirectives(_createElementVNode(\\"input\\", { _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 */), [ }, null, 512 /* NEED_PATCH */), [
[_vModelText, count.value] [_vModelText, count.value]
]), ]),
_withDirectives(_createElementVNode(\\"input\\", { _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 */), [ }, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(maybe)] [_vModelText, _unref(maybe)]
]), ]),
_withDirectives(_createElementVNode(\\"input\\", { _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 */), [ }, null, 512 /* NEED_PATCH */), [
[_vModelText, _unref(lett)] [_vModelText, _unref(lett)]
]) ])

View File

@ -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: `<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>`
)
})
})

View File

@ -1,4 +1,4 @@
import { VNodeChild } from '../vnode' import { VNode, VNodeChild } from '../vnode'
import { isArray, isString, isObject } from '@vue/shared' import { isArray, isString, isObject } from '@vue/shared'
import { warn } from '../warning' import { warn } from '../warning'
@ -52,13 +52,17 @@ export function renderList<T>(
*/ */
export function renderList( export function renderList(
source: any, source: any,
renderItem: (...args: any[]) => VNodeChild renderItem: (...args: any[]) => VNodeChild,
cache?: any[],
index?: number
): VNodeChild[] { ): VNodeChild[] {
let ret: VNodeChild[] let ret: VNodeChild[]
const cached = (cache && cache[index!]) as VNode[] | undefined
if (isArray(source) || isString(source)) { if (isArray(source) || isString(source)) {
ret = new Array(source.length) ret = new Array(source.length)
for (let i = 0, l = source.length; i < l; i++) { 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') { } else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) { if (__DEV__ && !Number.isInteger(source)) {
@ -71,17 +75,23 @@ export function renderList(
} }
} else if (isObject(source)) { } else if (isObject(source)) {
if (source[Symbol.iterator as any]) { 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 { } else {
const keys = Object.keys(source) const keys = Object.keys(source)
ret = new Array(keys.length) ret = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) { for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i] const key = keys[i]
ret[i] = renderItem(source[key], key, i) ret[i] = renderItem(source[key], key, i, cached && cached[i])
} }
} }
} else { } else {
ret = [] ret = []
} }
if (cache) {
cache[index!] = ret
}
return ret return ret
} }

View File

@ -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
}

View File

@ -264,6 +264,7 @@ export { renderList } from './helpers/renderList'
export { toHandlers } from './helpers/toHandlers' export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot' export { renderSlot } from './helpers/renderSlot'
export { createSlots } from './helpers/createSlots' export { createSlots } from './helpers/createSlots'
export { withMemo, isMemoSame } from './helpers/withMemo'
export { export {
openBlock, openBlock,
createBlock, createBlock,

View File

@ -182,6 +182,9 @@ export interface VNode<
// application root node only // application root node only
appContext: AppContext | null appContext: AppContext | null
// v-for memo
memo?: any[]
} }
// Since v-if and v-for are the two possible ways node structure can dynamically // 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 // Only tracks when this value is > 0
// We are not using a simple boolean because this value may need to be // We are not using a simple boolean because this value may need to be
// incremented/decremented by nested usage of v-once (see below) // 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 * 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 // optimized normalization for template-compiled render fns
export function cloneIfMounted(child: VNode): VNode { 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) { export function normalizeChildren(vnode: VNode, children: unknown) {