]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-core): re-implement v-once to use cache mechanism
authorEvan You <yyx990803@gmail.com>
Wed, 23 Oct 2019 21:57:40 +0000 (17:57 -0400)
committerEvan You <yyx990803@gmail.com>
Wed, 23 Oct 2019 21:57:40 +0000 (17:57 -0400)
21 files changed:
packages/compiler-core/__tests__/__snapshots__/codegen.spec.ts.snap
packages/compiler-core/__tests__/codegen.spec.ts
packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap [new file with mode: 0644]
packages/compiler-core/__tests__/transforms/vModel.spec.ts
packages/compiler-core/__tests__/transforms/vOnce.spec.ts
packages/compiler-core/__tests__/transforms/vSlot.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-core/src/transforms/vFor.ts
packages/compiler-core/src/transforms/vModel.ts
packages/compiler-core/src/transforms/vOnce.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/createRenderer.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/vnode.ts
packages/shared/src/index.ts

index 851147e0fb14c2784d1ed9c5c925a9c4f975d68c..ae00fd8de518207a4b2a8e4de289c1bd656a70b5 100644 (file)
@@ -21,6 +21,20 @@ export default function render() {
 }"
 `;
 
+exports[`compiler: codegen CacheExpression w/ isVNode: true 1`] = `
+"
+export default function render() {
+  const _ctx = this
+  const _cache = _ctx.$cache
+  return _cache[1] || (
+    setBlockTracking(-1),
+    _cache[1] = foo,
+    setBlockTracking(1),
+    _cache[1]
+  )
+}"
+`;
+
 exports[`compiler: codegen ConditionalExpression 1`] = `
 "
 return function render() {
index 9d7f0365f6b519a7b4e0bb9038cb62c7c8243c62..d9de265464be6cedb211869b7e611bd9b6905849 100644 (file)
@@ -380,4 +380,33 @@ describe('compiler: codegen', () => {
     expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`)
     expect(code).toMatchSnapshot()
   })
+
+  test('CacheExpression w/ isVNode: true', () => {
+    const { code } = generate(
+      createRoot({
+        cached: 1,
+        codegenNode: createCacheExpression(
+          1,
+          createSimpleExpression(`foo`, false),
+          true
+        )
+      }),
+      {
+        mode: 'module',
+        prefixIdentifiers: true
+      }
+    )
+    expect(code).toMatch(`const _cache = _ctx.$cache`)
+    expect(code).toMatch(
+      `
+  _cache[1] || (
+    setBlockTracking(-1),
+    _cache[1] = foo,
+    setBlockTracking(1),
+    _cache[1]
+  )
+    `.trim()
+    )
+    expect(code).toMatchSnapshot()
+  })
 })
diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap
new file mode 100644 (file)
index 0000000..4c63127
--- /dev/null
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`compiler: v-once transform as root node 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { setBlockTracking: _setBlockTracking, createVNode: _createVNode } = _Vue
+    const _cache = $cache
+    
+    return _cache[1] || (
+      _setBlockTracking(-1),
+      _cache[1] = _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
+      _setBlockTracking(1),
+      _cache[1]
+    )
+  }
+}"
+`;
+
+exports[`compiler: v-once transform on component 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { setBlockTracking: _setBlockTracking, resolveComponent: _resolveComponent, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    const _cache = $cache
+    
+    const _component_Comp = _resolveComponent(\\"Comp\\")
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _cache[1] || (
+        _setBlockTracking(-1),
+        _cache[1] = _createVNode(_component_Comp, { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
+        _setBlockTracking(1),
+        _cache[1]
+      )
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: v-once transform on nested plain element 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    const _cache = $cache
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _cache[1] || (
+        _setBlockTracking(-1),
+        _cache[1] = _createVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"]),
+        _setBlockTracking(1),
+        _cache[1]
+      )
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: v-once transform on slot outlet 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { setBlockTracking: _setBlockTracking, renderSlot: _renderSlot, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    const _cache = $cache
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _cache[1] || (
+        _setBlockTracking(-1),
+        _cache[1] = _renderSlot($slots, \\"default\\"),
+        _setBlockTracking(1),
+        _cache[1]
+      )
+    ]))
+  }
+}"
+`;
+
+exports[`compiler: v-once transform with hoistStatic: true 1`] = `
+"const _Vue = Vue
+
+return function render() {
+  with (this) {
+    const { setBlockTracking: _setBlockTracking, createVNode: _createVNode, createBlock: _createBlock, openBlock: _openBlock } = _Vue
+    const _cache = $cache
+    
+    return (_openBlock(), _createBlock(\\"div\\", null, [
+      _cache[1] || (
+        _setBlockTracking(-1),
+        _cache[1] = _createVNode(\\"div\\"),
+        _setBlockTracking(1),
+        _cache[1]
+      )
+    ]))
+  }
+}"
+`;
index 10ee06413fd133ffbc69871bffa715b0437a3ee1..a44b16c80f9d243e90ae025d8fb8f1b89bcd31d4 100644 (file)
@@ -387,7 +387,8 @@ describe('compiler: transform v-model', () => {
     const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', {
       prefixIdentifiers: true
     })
-    const args = (root.children[0] as ComponentNode).codegenNode!.arguments
+    const args = ((root.children[0] as ComponentNode)
+      .codegenNode as CallExpression).arguments
     // props
     expect(args[1]).toMatchObject({
       properties: [
index e0533b4f4d32788ab801786e40ea38675eadada5..5d2a2a05fc644e931a694b74c42fb27091ab4e51 100644 (file)
-import { parse, transform, ElementNode, CallExpression } from '../../src'
+import {
+  parse,
+  transform,
+  NodeTypes,
+  generate,
+  CompilerOptions
+} from '../../src'
 import { transformOnce } from '../../src/transforms/vOnce'
 import { transformElement } from '../../src/transforms/transformElement'
-import { createObjectMatcher } from '../testUtils'
+import {
+  CREATE_VNODE,
+  RENDER_SLOT,
+  SET_BLOCK_TRACKING
+} from '../../src/runtimeHelpers'
+import { transformBind } from '../../src/transforms/vBind'
+import { transformSlotOutlet } from '../../src/transforms/transformSlotOutlet'
 
-function transformWithOnce(template: string) {
+function transformWithOnce(template: string, options: CompilerOptions = {}) {
   const ast = parse(template)
   transform(ast, {
-    nodeTransforms: [transformElement],
+    nodeTransforms: [transformOnce, transformElement, transformSlotOutlet],
     directiveTransforms: {
-      once: transformOnce
-    }
+      bind: transformBind
+    },
+    ...options
   })
-  return ast.children[0] as ElementNode
+  return ast
 }
 
 describe('compiler: v-once transform', () => {
-  test('should add no props to DOM', () => {
-    const node = transformWithOnce(`<div v-once />`)
-    const codegenArgs = (node.codegenNode as CallExpression).arguments
+  test('as root node', () => {
+    const root = transformWithOnce(`<div :id="foo" v-once />`)
+    expect(root.cached).toBe(1)
+    expect(root.helpers).toContain(SET_BLOCK_TRACKING)
+    expect(root.codegenNode).toMatchObject({
+      type: NodeTypes.JS_CACHE_EXPRESSION,
+      index: 1,
+      value: {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE
+      }
+    })
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('on nested plain element', () => {
+    const root = transformWithOnce(`<div><div :id="foo" v-once /></div>`)
+    expect(root.cached).toBe(1)
+    expect(root.helpers).toContain(SET_BLOCK_TRACKING)
+    expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
+      type: NodeTypes.JS_CACHE_EXPRESSION,
+      index: 1,
+      value: {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE
+      }
+    })
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('on component', () => {
+    const root = transformWithOnce(`<div><Comp :id="foo" v-once /></div>`)
+    expect(root.cached).toBe(1)
+    expect(root.helpers).toContain(SET_BLOCK_TRACKING)
+    expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
+      type: NodeTypes.JS_CACHE_EXPRESSION,
+      index: 1,
+      value: {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE
+      }
+    })
+    expect(generate(root).code).toMatchSnapshot()
+  })
+
+  test('on slot outlet', () => {
+    const root = transformWithOnce(`<div><slot v-once /></div>`)
+    expect(root.cached).toBe(1)
+    expect(root.helpers).toContain(SET_BLOCK_TRACKING)
+    expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
+      type: NodeTypes.JS_CACHE_EXPRESSION,
+      index: 1,
+      value: {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: RENDER_SLOT
+      }
+    })
+    expect(generate(root).code).toMatchSnapshot()
+  })
 
-    expect(codegenArgs[1]).toMatchObject(
-      createObjectMatcher({
-        $once: `[true]`
-      })
-    )
+  // cached nodes should be ignored by hoistStatic transform
+  test('with hoistStatic: true', () => {
+    const root = transformWithOnce(`<div><div v-once /></div>`, {
+      hoistStatic: true
+    })
+    expect(root.cached).toBe(1)
+    expect(root.helpers).toContain(SET_BLOCK_TRACKING)
+    expect(root.hoists.length).toBe(0)
+    expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
+      type: NodeTypes.JS_CACHE_EXPRESSION,
+      index: 1,
+      value: {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_VNODE
+      }
+    })
+    expect(generate(root).code).toMatchSnapshot()
   })
 })
index b25897d4dc9dfe3313f1b1b8dc237975d6a1953b..4ba645cc98fa648650c8a30e84020fc63509ec03 100644 (file)
@@ -365,7 +365,7 @@ describe('compiler: transform component slots', () => {
       } else {
         const innerComp = (root.children[0] as ComponentNode)
           .children[0] as ComponentNode
-        flag = innerComp.codegenNode!.arguments[3]
+        flag = (innerComp.codegenNode as CallExpression).arguments[3]
       }
       if (shouldForce) {
         expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS))
index 740da9b1584f4f223593b826dc6d33d540363c8e..4a53de662fed07dfb06c7524ac543788bca3c1d2 100644 (file)
@@ -116,40 +116,45 @@ export interface BaseElementNode extends Node {
   isSelfClosing: boolean
   props: Array<AttributeNode | DirectiveNode>
   children: TemplateChildNode[]
-  codegenNode: CallExpression | SimpleExpressionNode | undefined
+  codegenNode:
+    | CallExpression
+    | SimpleExpressionNode
+    | CacheExpression
+    | undefined
 }
 
 export interface PlainElementNode extends BaseElementNode {
   tagType: ElementTypes.ELEMENT
-  codegenNode: ElementCodegenNode | undefined | SimpleExpressionNode // only when hoisted
+  codegenNode:
+    | ElementCodegenNode
+    | undefined
+    | SimpleExpressionNode // when hoisted
+    | CacheExpression // when cached by v-once
 }
 
 export interface ComponentNode extends BaseElementNode {
   tagType: ElementTypes.COMPONENT
-  codegenNode: ComponentCodegenNode | undefined
+  codegenNode: ComponentCodegenNode | undefined | CacheExpression // when cached by v-once
 }
 
 export interface SlotOutletNode extends BaseElementNode {
   tagType: ElementTypes.SLOT
-  codegenNode: SlotOutletCodegenNode | undefined
+  codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
 }
 
 export interface TemplateNode extends BaseElementNode {
   tagType: ElementTypes.TEMPLATE
-  codegenNode:
-    | ElementCodegenNode
-    | CodegenNodeWithDirective<ElementCodegenNode>
-    | undefined
+  codegenNode: ElementCodegenNode | undefined | CacheExpression
 }
 
 export interface PortalNode extends BaseElementNode {
   tagType: ElementTypes.PORTAL
-  codegenNode: ElementCodegenNode | undefined
+  codegenNode: ElementCodegenNode | undefined | CacheExpression
 }
 
 export interface SuspenseNode extends BaseElementNode {
   tagType: ElementTypes.SUSPENSE
-  codegenNode: ElementCodegenNode | undefined
+  codegenNode: ElementCodegenNode | undefined | CacheExpression
 }
 
 export interface TextNode extends Node {
@@ -298,6 +303,7 @@ export interface CacheExpression extends Node {
   type: NodeTypes.JS_CACHE_EXPRESSION
   index: number
   value: JSChildNode
+  isVNode: boolean
 }
 
 // Codegen Node Types ----------------------------------------------------------
@@ -625,12 +631,14 @@ export function createConditionalExpression(
 
 export function createCacheExpression(
   index: number,
-  value: JSChildNode
+  value: JSChildNode,
+  isVNode: boolean = false
 ): CacheExpression {
   return {
     type: NodeTypes.JS_CACHE_EXPRESSION,
     index,
     value,
+    isVNode,
     loc: locStub
   }
 }
index ba49ad16d7a1a7cf21eafa7cbd1f20cadb5ea6e4..04384ceb65a62bc59ecc05eec75b360ab452807b 100644 (file)
@@ -34,7 +34,8 @@ import {
   COMMENT,
   helperNameMap,
   RESOLVE_COMPONENT,
-  RESOLVE_DIRECTIVE
+  RESOLVE_DIRECTIVE,
+  SET_BLOCK_TRACKING
 } from './runtimeHelpers'
 
 type CodegenNode = TemplateChildNode | JSChildNode
@@ -247,6 +248,10 @@ export function generate(
           .join(', ')} } = _Vue`
       )
       newline()
+      if (ast.cached > 0) {
+        push(`const _cache = $cache`)
+        newline()
+      }
       newline()
     }
   } else {
@@ -625,7 +630,22 @@ function genSequenceExpression(
 }
 
 function genCacheExpression(node: CacheExpression, context: CodegenContext) {
-  context.push(`_cache[${node.index}] || (_cache[${node.index}] = `)
+  const { push, helper, indent, deindent, newline } = context
+  push(`_cache[${node.index}] || (`)
+  if (node.isVNode) {
+    indent()
+    push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
+    newline()
+  }
+  push(`_cache[${node.index}] = `)
   genNode(node.value, context)
-  context.push(`)`)
+  if (node.isVNode) {
+    push(`,`)
+    newline()
+    push(`${helper(SET_BLOCK_TRACKING)}(1),`)
+    newline()
+    push(`_cache[${node.index}]`)
+    deindent()
+  }
+  push(`)`)
 }
index 097f891733ec0d0cb9566651bbeb7035fdcfe6d3..5b34d66eb64e0f34ece388e3784ad349acf22d7f 100644 (file)
@@ -44,6 +44,7 @@ export function baseCompile(
     ...options,
     prefixIdentifiers,
     nodeTransforms: [
+      transformOnce,
       transformIf,
       transformFor,
       ...(prefixIdentifiers
@@ -62,7 +63,6 @@ export function baseCompile(
     directiveTransforms: {
       on: transformOn,
       bind: transformBind,
-      once: transformOnce,
       model: transformModel,
       ...(options.directiveTransforms || {}) // user transforms
     }
index 6cd690ced779d9db36464a193cede44030d505b2..a2ed747ba46b3db0eb076c13ca19f78b01990f10 100644 (file)
@@ -19,6 +19,7 @@ export const TO_STRING = Symbol(__DEV__ ? `toString` : ``)
 export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
 export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
 export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
+export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
 
 // Name mapping for runtime helpers that need to be imported from 'vue' in
 // generated code. Make sure these are correctly exported in the runtime!
@@ -42,7 +43,8 @@ export const helperNameMap: any = {
   [TO_STRING]: `toString`,
   [MERGE_PROPS]: `mergeProps`,
   [TO_HANDLERS]: `toHandlers`,
-  [CAMELIZE]: `camelize`
+  [CAMELIZE]: `camelize`,
+  [SET_BLOCK_TRACKING]: `setBlockTracking`
 }
 
 export function registerRuntimeHelpers(helpers: any) {
index fe717905cef681d8a3e62c4a3a1e0b0849a7b8a5..f00656e81f3108673c33d366967633efaf3c15c2 100644 (file)
@@ -100,7 +100,7 @@ export interface TransformContext extends Required<TransformOptions> {
   addIdentifiers(exp: ExpressionNode | string): void
   removeIdentifiers(exp: ExpressionNode | string): void
   hoist(exp: JSChildNode): SimpleExpressionNode
-  cache<T extends JSChildNode>(exp: T): CacheExpression | T
+  cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
 }
 
 function createTransformContext(
@@ -219,8 +219,8 @@ function createTransformContext(
         true
       )
     },
-    cache(exp) {
-      return cacheHandlers ? createCacheExpression(++context.cached, exp) : exp
+    cache(exp, isVNode = false) {
+      return createCacheExpression(++context.cached, exp, isVNode)
     }
   }
 
@@ -260,12 +260,17 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
       const codegenNode = child.codegenNode as
         | ElementCodegenNode
         | ComponentCodegenNode
-      if (codegenNode.callee === WITH_DIRECTIVES) {
-        codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
+        | CacheExpression
+      if (codegenNode.type !== NodeTypes.JS_CACHE_EXPRESSION) {
+        if (codegenNode.callee === WITH_DIRECTIVES) {
+          codegenNode.arguments[0].callee = helper(CREATE_BLOCK)
+        } else {
+          codegenNode.callee = helper(CREATE_BLOCK)
+        }
+        root.codegenNode = createBlockExpression(codegenNode, context)
       } else {
-        codegenNode.callee = helper(CREATE_BLOCK)
+        root.codegenNode = codegenNode
       }
-      root.codegenNode = createBlockExpression(codegenNode, context)
     } else {
       // - single <slot/>, IfNode, ForNode: already blocks.
       // - single text node: always patched.
index 9ef9dda2a0f038769c5dc8d37dc465dd6a7b9e93..7dd38b4650999f2e5ee68308ab5791f333841804 100644 (file)
@@ -4,12 +4,12 @@ import {
   TemplateChildNode,
   SimpleExpressionNode,
   ElementTypes,
-  ElementCodegenNode,
   PlainElementNode,
   ComponentNode,
   TemplateNode,
   ElementNode,
-  PlainElementCodegenNode
+  PlainElementCodegenNode,
+  CodegenNodeWithDirective
 } from '../ast'
 import { TransformContext } from '../transform'
 import { WITH_DIRECTIVES } from '../runtimeHelpers'
@@ -57,17 +57,20 @@ function walk(
       } else {
         // node may contain dynamic children, but its props may be eligible for
         // hoisting.
-        const flag = getPatchFlag(child)
-        if (
-          (!flag ||
-            flag === PatchFlags.NEED_PATCH ||
-            flag === PatchFlags.TEXT) &&
-          !hasDynamicKeyOrRef(child) &&
-          !hasCachedProps(child)
-        ) {
-          const props = getNodeProps(child)
-          if (props && props !== `null`) {
-            getVNodeCall(child).arguments[1] = context.hoist(props)
+        const codegenNode = child.codegenNode!
+        if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
+          const flag = getPatchFlag(codegenNode)
+          if (
+            (!flag ||
+              flag === PatchFlags.NEED_PATCH ||
+              flag === PatchFlags.TEXT) &&
+            !hasDynamicKeyOrRef(child) &&
+            !hasCachedProps(child)
+          ) {
+            const props = getNodeProps(child)
+            if (props && props !== `null`) {
+              getVNodeCall(codegenNode).arguments[1] = context.hoist(props)
+            }
           }
         }
       }
@@ -100,7 +103,11 @@ export function isStaticNode(
       if (cached !== undefined) {
         return cached
       }
-      const flag = getPatchFlag(node)
+      const codegenNode = node.codegenNode!
+      if (codegenNode.type !== NodeTypes.JS_CALL_EXPRESSION) {
+        return false
+      }
+      const flag = getPatchFlag(codegenNode)
       if (!flag && !hasDynamicKeyOrRef(node) && !hasCachedProps(node)) {
         // element self is static. check its children.
         for (let i = 0; i < node.children.length; i++) {
@@ -165,26 +172,32 @@ function hasCachedProps(node: PlainElementNode): boolean {
   return false
 }
 
-function getVNodeCall(node: PlainElementNode) {
-  let codegenNode = node.codegenNode as ElementCodegenNode
-  if (codegenNode.callee === WITH_DIRECTIVES) {
-    codegenNode = codegenNode.arguments[0]
+function getNodeProps(node: PlainElementNode) {
+  const codegenNode = node.codegenNode!
+  if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
+    return getVNodeArgAt(
+      codegenNode,
+      1
+    ) as PlainElementCodegenNode['arguments'][1]
   }
-  return codegenNode
 }
 
+type NonCachedCodegenNode =
+  | PlainElementCodegenNode
+  | CodegenNodeWithDirective<PlainElementCodegenNode>
+
 function getVNodeArgAt(
-  node: PlainElementNode,
+  node: NonCachedCodegenNode,
   index: number
 ): PlainElementCodegenNode['arguments'][number] {
   return getVNodeCall(node).arguments[index]
 }
 
-function getPatchFlag(node: PlainElementNode): number | undefined {
-  const flag = getVNodeArgAt(node, 3) as string
-  return flag ? parseInt(flag, 10) : undefined
+function getVNodeCall(node: NonCachedCodegenNode) {
+  return node.callee === WITH_DIRECTIVES ? node.arguments[0] : node
 }
 
-function getNodeProps(node: PlainElementNode) {
-  return getVNodeArgAt(node, 1) as PlainElementCodegenNode['arguments'][1]
+function getPatchFlag(node: NonCachedCodegenNode): number | undefined {
+  const flag = getVNodeArgAt(node, 3) as string
+  return flag ? parseInt(flag, 10) : undefined
 }
index 646a51ce61931b53e5c042f595789602ac1e4e87..9fba3d82fa00ff090a2c58dedc059007d3aa61fa 100644 (file)
@@ -280,6 +280,11 @@ export function buildProps(
         continue
       }
 
+      // skip v-once - it is handled by its dedicated transform.
+      if (name === 'once') {
+        continue
+      }
+
       // special case for v-bind and v-on with no argument
       const isBind = name === 'bind'
       const isOn = name === 'on'
index 2f060a8985f9ae763e1de0ac22c0a1edccd3b3cf..6e4f29ce0828c35f5cea3a4c5664b9287a67dafc 100644 (file)
@@ -15,7 +15,8 @@ import {
   createObjectExpression,
   createObjectProperty,
   ForCodegenNode,
-  ElementCodegenNode
+  ElementCodegenNode,
+  SlotOutletCodegenNode
 } from '../ast'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
@@ -130,7 +131,7 @@ export const transformFor = createStructuralDirectiveTransform(
         : null
       if (slotOutlet) {
         // <slot v-for="..."> or <template v-for="..."><slot/></template>
-        childBlock = slotOutlet.codegenNode!
+        childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
         if (isTemplate && keyProperty) {
           // <template v-for="..." :key="..."><slot/></template>
           // we need to inject the key to the renderSlot() call.
index 81c917203e3ceee4037ab6114d6b6d7766769f32..a0b7f1107e7509f2bbe8bf8725adf2009f57276d 100644 (file)
@@ -69,6 +69,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
   if (
     !__BROWSER__ &&
     context.prefixIdentifiers &&
+    context.cacheHandlers &&
     !hasScopeRef(exp, context.identifiers)
   ) {
     props[1].value = context.cache(props[1].value)
index 170a24c3cee6b10936e3286de269af1a6889b386..f9d46f1a1f15b137725edb3f7427878b2f3849f3 100644 (file)
@@ -1,17 +1,15 @@
-import {
-  DirectiveTransform,
-  createObjectProperty,
-  createSimpleExpression
-} from '@vue/compiler-core'
+import { NodeTransform } from '../transform'
+import { findDir } from '../utils'
+import { NodeTypes } from '../ast'
+import { SET_BLOCK_TRACKING } from '../runtimeHelpers'
 
-export const transformOnce: DirectiveTransform = dir => {
-  return {
-    props: [
-      createObjectProperty(
-        createSimpleExpression(`$once`, true, dir.loc),
-        createSimpleExpression('true', false)
-      )
-    ],
-    needRuntime: false
+export const transformOnce: NodeTransform = (node, context) => {
+  if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
+    context.helper(SET_BLOCK_TRACKING)
+    return () => {
+      if (node.codegenNode) {
+        node.codegenNode = context.cache(node.codegenNode, true /* isVNode */)
+      }
+    }
   }
 }
index 909d04ce8e9255cc22341111ee833742775a0779..b0debfecaa9a3b507c50569b276e7c9f14b19a3d 100644 (file)
@@ -86,7 +86,7 @@ export interface ComponentInternalInstance {
   accessCache: Data | null
   // cache for render function values that rely on _ctx but won't need updates
   // after initialized (e.g. inline handlers)
-  renderCache: Function[] | null
+  renderCache: (Function | VNode)[] | null
 
   components: Record<string, Component>
   directives: Record<string, Directive>
index 16ca8e8da3959d0ed734114703f152f5dbdc83ed..5508c38fbdf65a2505317a54bd3d0e3fd3393278 100644 (file)
@@ -179,14 +179,10 @@ export function createRenderer<
     optimized: boolean = false
   ) {
     // patching & not same type, unmount old tree
-    if (n1 != null) {
-      if (!isSameType(n1, n2)) {
-        anchor = getNextHostNode(n1)
-        unmount(n1, parentComponent, parentSuspense, true)
-        n1 = null
-      } else if (n1.props && n1.props.$once) {
-        return
-      }
+    if (n1 != null && !isSameType(n1, n2)) {
+      anchor = getNextHostNode(n1)
+      unmount(n1, parentComponent, parentSuspense, true)
+      n1 = null
     }
 
     const { type, shapeFlag } = n2
index d55cc7b142d2d8aa8d8955d4aa8e00cca4db79da..ceb4c6b29501fc3276e4296bed8f15b7bcd043bd 100644 (file)
@@ -49,6 +49,7 @@ export { toString } from './helpers/toString'
 export { toHandlers } from './helpers/toHandlers'
 export { renderSlot } from './helpers/renderSlot'
 export { createSlots } from './helpers/createSlots'
+export { setBlockTracking } from './vnode'
 export { capitalize, camelize } from '@vue/shared'
 
 // Internal, for integration with runtime compiler
index 632b160def3b6542b4f11edd9353bc15d9c02ac5..2f167b647ffcf916ce776d21e1089177da6afd46 100644 (file)
@@ -105,7 +105,24 @@ export function openBlock(disableTracking?: boolean) {
   blockStack.push((currentBlock = disableTracking ? null : []))
 }
 
-let shouldTrack = true
+// Whether we should be tracking dynamic child nodes inside a block.
+// 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 shouldTrack = 1
+
+// Block tracking sometimes needs to be disabled, for example during the
+// creation of a tree that needs to be cached by v-once. The compiler generates
+// code like this:
+//   _cache[1] || (
+//     setBlockTracking(-1),
+//     _cache[1] = createVNode(...),
+//     setBlockTracking(1),
+//     _cache[1]
+//   )
+export function setBlockTracking(value: number) {
+  shouldTrack += value
+}
 
 // Create a block root vnode. Takes the same exact arguments as `createVNode`.
 // A block root keeps track of dynamic nodes within the block in the
@@ -118,9 +135,9 @@ export function createBlock(
   dynamicProps?: string[]
 ): VNode {
   // avoid a block with patchFlag tracking itself
-  shouldTrack = false
+  shouldTrack--
   const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
-  shouldTrack = true
+  shouldTrack++
   // save current block children on the block vnode
   vnode.dynamicChildren = currentBlock || EMPTY_ARR
   // close block
@@ -200,7 +217,7 @@ export function createVNode(
   // component doesn't need to update, it needs to persist the instance on to
   // the next vnode so that it can be properly unmounted later.
   if (
-    shouldTrack &&
+    shouldTrack > 0 &&
     currentBlock !== null &&
     (patchFlag > 0 ||
       shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
index 32836f53aee8360f0bf60c0071b15463c78d0a5f..5c7cc6a2ed83a861c8c27eb7780cc5ae49c0cdba 100644 (file)
@@ -52,7 +52,7 @@ export const isPlainObject = (val: unknown): val is object =>
   toTypeString(val) === '[object Object]'
 
 export const isReservedProp = (key: string): boolean =>
-  key === 'key' || key === 'ref' || key === '$once' || key.startsWith(`onVnode`)
+  key === 'key' || key === 'ref' || key.startsWith(`onVnode`)
 
 const camelizeRE = /-(\w)/g
 export const camelize = (str: string): string => {