]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat: v-memo
authorEvan You <yyx990803@gmail.com>
Sat, 10 Jul 2021 01:41:44 +0000 (21:41 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 16 Jul 2021 18:30:49 +0000 (14:30 -0400)
23 files changed:
packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
packages/compiler-core/__tests__/transforms/__snapshots__/vMemo.spec.ts.snap [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
packages/compiler-core/__tests__/transforms/vMemo.spec.ts [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/vOn.spec.ts
packages/compiler-core/__tests__/transforms/vOnce.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/compile.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vIf.ts
packages/compiler-core/src/transforms/vMemo.ts [new file with mode: 0644]
packages/compiler-core/src/utils.ts
packages/compiler-dom/__tests__/transforms/vOn.spec.ts
packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/runtime-core/__tests__/helpers/withMemo.spec.ts [new file with mode: 0644]
packages/runtime-core/src/helpers/renderList.ts
packages/runtime-core/src/helpers/withMemo.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts
packages/runtime-core/src/vnode.ts

index 4ee25d5ac2d74bbc52781ce556317dbb137d8433..0a6ca2322011e620a0fae54cdf7a1a8180225e0b 100644 (file)
@@ -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 (file)
index 0000000..7e65428
--- /dev/null
@@ -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)
+  ]))
+}"
+`;
index db9e9980f907483e3bff28ab4064bb26f012e8e3..575c59ebb59978e82daa3df1023783d5874fd631 100644 (file)
@@ -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 (file)
index 0000000..1b259f7
--- /dev/null
@@ -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()
+  })
+})
index 85b1b93b7b53cf04c9de80ec7c8157ce5f15350b..24789d16f83fc817e6244f709a153526a5c60a4a 100644 (file)
@@ -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: [
index a18a0947d00916e1acaa89bb9e749067673af504..d3b74f92409396e242bd5ee52c4e3244cbc835dc 100644 (file)
@@ -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"`
index eaf48666b5e298ed72bd50003fa6848e7d6874b7..b087a9845d631689caf4397358c6ad0b8dadba78 100644 (file)
@@ -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 {
index d930c36cd63d54de9920f391b39c827ea0cb5cda..eeb13baf83a54c9b42d27d83cce66496df551231 100644 (file)
@@ -651,11 +651,11 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
     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
index 78a6cb62e06d2ff322e585d1defeb3fc3fa74dd3..fa2dcfbe4604a5cef645065ec2f28d1061f6e1e6 100644 (file)
@@ -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
index d9f7d6c3e597af16367ade491542c73631689de4..eab6753433198cd6ac11a3882117787600537041 100644 (file)
@@ -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) {
index 44e06fc22ca7934563da9a6084437cb97e59cd9d..eb328dcd257659d3c617321ff46496606a74d7c9 100644 (file)
@@ -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 {
index 0b10b70a2d8b0c53e0eac8cfb2c0d0c5e494f520..697e8e294a2dfb92d3082e457c83170db3a23a88 100644 (file)
@@ -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 <component>
index a44b724b604c209492afb17a6381a4a6c4c66b6a..e4b02eaab81bd24639d9624b76f9488cc2a66a65 100644 (file)
@@ -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)
+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))
 }
index 0ce5c540d8203a36a2537d3ec5c0177bafef0699..01810090c4436715bb0133f6dc281221478ae8bb 100644 (file)
@@ -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 (file)
index 0000000..4e15087
--- /dev/null
@@ -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
+      }
+    }
+  }
+}
index b0aca02d830eb2ef1024ecc5ddf1b4d4fe6dcff5..b02379178df3a7931c83f47adc4305fc32bd2dd9 100644 (file)
@@ -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))
+  }
+}
index 84896a60d759341cec7778426740c3e502c8041b..b148422b8b2b2717e88b4c563ce8e5a319ba853f 100644 (file)
@@ -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
index ecf1e4696d36fab00dfa3bca1d9196325dc15469..d712b9df3d55a7ec13eb3f0ec17e69edc17d8896 100644 (file)
@@ -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 (file)
index 0000000..b98b8c2
--- /dev/null
@@ -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>`
+    )
+  })
+})
index de4ab8afa7f7ad004269da3e6505cbed91ea22cc..543c343ddace9a93441982def6cc4123b4c177ab 100644 (file)
@@ -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<T>(
  */
 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<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
 }
diff --git a/packages/runtime-core/src/helpers/withMemo.ts b/packages/runtime-core/src/helpers/withMemo.ts
new file mode 100644 (file)
index 0000000..33a243c
--- /dev/null
@@ -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
+}
index 3b745735542ede660291d45efdb9c73407900f47..5d3efba096f2276c84dcb7b70e83f8eeedac4232 100644 (file)
@@ -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,
index 89d3507aa32c0831cb7b6c1580053d3dc51e0088..aceaf3a90128408d962214018a15ac90711147b9 100644 (file)
@@ -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) {