From 3b64508e3b2d648e346cbf34e1641f4022be61b6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 9 Jul 2021 21:41:44 -0400 Subject: [PATCH] feat: v-memo --- .../__snapshots__/hoistStatic.spec.ts.snap | 2 +- .../__snapshots__/vMemo.spec.ts.snap | 82 ++++++++++ .../__snapshots__/vOnce.spec.ts.snap | 30 ++-- .../__tests__/transforms/vMemo.spec.ts | 56 +++++++ .../__tests__/transforms/vOn.spec.ts | 10 +- .../__tests__/transforms/vOnce.spec.ts | 10 +- packages/compiler-core/src/ast.ts | 20 ++- packages/compiler-core/src/codegen.ts | 6 +- packages/compiler-core/src/compile.ts | 2 + packages/compiler-core/src/runtimeHelpers.ts | 6 +- packages/compiler-core/src/transform.ts | 18 +-- .../src/transforms/transformElement.ts | 4 +- packages/compiler-core/src/transforms/vFor.ts | 107 ++++++++----- packages/compiler-core/src/transforms/vIf.ts | 30 ++-- .../compiler-core/src/transforms/vMemo.ts | 40 +++++ packages/compiler-core/src/utils.ts | 28 +++- .../__tests__/transforms/vOn.spec.ts | 2 +- .../__snapshots__/compileScript.spec.ts.snap | 34 ++-- .../__tests__/helpers/withMemo.spec.ts | 150 ++++++++++++++++++ .../runtime-core/src/helpers/renderList.ts | 20 ++- packages/runtime-core/src/helpers/withMemo.ts | 29 ++++ packages/runtime-core/src/index.ts | 1 + packages/runtime-core/src/vnode.ts | 7 +- 23 files changed, 563 insertions(+), 131 deletions(-) create mode 100644 packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap create mode 100644 packages/compiler-core/__tests__/transforms/vMemo.spec.ts create mode 100644 packages/compiler-core/src/transforms/vMemo.ts create mode 100644 packages/runtime-core/__tests__/helpers/withMemo.spec.ts create mode 100644 packages/runtime-core/src/helpers/withMemo.ts 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 4ee25d5a..0a6ca232 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 00000000..7e654286 --- /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 db9e9980..575c59eb 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 00000000..1b259f7c --- /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( + `` + ) + ).toMatchSnapshot() + }) +}) diff --git a/packages/compiler-core/__tests__/transforms/vOn.spec.ts b/packages/compiler-core/__tests__/transforms/vOn.spec.ts index 85b1b93b..24789d16 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 a18a0947..d3b74f92 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 eaf48666..b087a984 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 d930c36c..eeb13baf 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 78a6cb62..fa2dcfbe 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 d9f7d6c3..eab67534 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 44e06fc2..eb328dcd 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 0b10b70a..697e8e29 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 a44b724b..e4b02eaa 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 0ce5c540..01810090 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 00000000..4e150875 --- /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 b0aca02d..b0237917 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 84896a60..b148422b 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 ecf1e469..d712b9df 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 00000000..b98b8c29 --- /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 de4ab8af..543c343d 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 00000000..33a243c9 --- /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 3b745735..5d3efba0 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 89d3507a..aceaf3a9 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) {