feat(compiler-core): re-implement v-once to use cache mechanism

This commit is contained in:
Evan You 2019-10-23 17:57:40 -04:00
parent 9291011456
commit af5a8e1154
21 changed files with 388 additions and 95 deletions

View 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`] = ` exports[`compiler: codegen ConditionalExpression 1`] = `
" "
return function render() { return function render() {

View File

@ -380,4 +380,33 @@ describe('compiler: codegen', () => {
expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`) expect(code).toMatch(`_cache[1] || (_cache[1] = foo)`)
expect(code).toMatchSnapshot() 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()
})
}) })

View File

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

View File

@ -387,7 +387,8 @@ describe('compiler: transform v-model', () => {
const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', { const root = parseWithVModel('<Comp v-model.trim.bar-baz="foo" />', {
prefixIdentifiers: true prefixIdentifiers: true
}) })
const args = (root.children[0] as ComponentNode).codegenNode!.arguments const args = ((root.children[0] as ComponentNode)
.codegenNode as CallExpression).arguments
// props // props
expect(args[1]).toMatchObject({ expect(args[1]).toMatchObject({
properties: [ properties: [

View File

@ -1,28 +1,109 @@
import { parse, transform, ElementNode, CallExpression } from '../../src' import {
parse,
transform,
NodeTypes,
generate,
CompilerOptions
} from '../../src'
import { transformOnce } from '../../src/transforms/vOnce' import { transformOnce } from '../../src/transforms/vOnce'
import { transformElement } from '../../src/transforms/transformElement' 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) const ast = parse(template)
transform(ast, { transform(ast, {
nodeTransforms: [transformElement], nodeTransforms: [transformOnce, transformElement, transformSlotOutlet],
directiveTransforms: { directiveTransforms: {
once: transformOnce bind: transformBind
} },
...options
}) })
return ast.children[0] as ElementNode return ast
} }
describe('compiler: v-once transform', () => { describe('compiler: v-once transform', () => {
test('should add no props to DOM', () => { test('as root node', () => {
const node = transformWithOnce(`<div v-once />`) const root = transformWithOnce(`<div :id="foo" v-once />`)
const codegenArgs = (node.codegenNode as CallExpression).arguments 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()
})
expect(codegenArgs[1]).toMatchObject( test('on nested plain element', () => {
createObjectMatcher({ const root = transformWithOnce(`<div><div :id="foo" v-once /></div>`)
$once: `[true]` 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()
})
// 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()
}) })
}) })

View File

@ -365,7 +365,7 @@ describe('compiler: transform component slots', () => {
} else { } else {
const innerComp = (root.children[0] as ComponentNode) const innerComp = (root.children[0] as ComponentNode)
.children[0] as ComponentNode .children[0] as ComponentNode
flag = innerComp.codegenNode!.arguments[3] flag = (innerComp.codegenNode as CallExpression).arguments[3]
} }
if (shouldForce) { if (shouldForce) {
expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS)) expect(flag).toBe(genFlagText(PatchFlags.DYNAMIC_SLOTS))

View File

@ -116,40 +116,45 @@ export interface BaseElementNode extends Node {
isSelfClosing: boolean isSelfClosing: boolean
props: Array<AttributeNode | DirectiveNode> props: Array<AttributeNode | DirectiveNode>
children: TemplateChildNode[] children: TemplateChildNode[]
codegenNode: CallExpression | SimpleExpressionNode | undefined codegenNode:
| CallExpression
| SimpleExpressionNode
| CacheExpression
| undefined
} }
export interface PlainElementNode extends BaseElementNode { export interface PlainElementNode extends BaseElementNode {
tagType: ElementTypes.ELEMENT 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 { export interface ComponentNode extends BaseElementNode {
tagType: ElementTypes.COMPONENT tagType: ElementTypes.COMPONENT
codegenNode: ComponentCodegenNode | undefined codegenNode: ComponentCodegenNode | undefined | CacheExpression // when cached by v-once
} }
export interface SlotOutletNode extends BaseElementNode { export interface SlotOutletNode extends BaseElementNode {
tagType: ElementTypes.SLOT tagType: ElementTypes.SLOT
codegenNode: SlotOutletCodegenNode | undefined codegenNode: SlotOutletCodegenNode | undefined | CacheExpression // when cached by v-once
} }
export interface TemplateNode extends BaseElementNode { export interface TemplateNode extends BaseElementNode {
tagType: ElementTypes.TEMPLATE tagType: ElementTypes.TEMPLATE
codegenNode: codegenNode: ElementCodegenNode | undefined | CacheExpression
| ElementCodegenNode
| CodegenNodeWithDirective<ElementCodegenNode>
| undefined
} }
export interface PortalNode extends BaseElementNode { export interface PortalNode extends BaseElementNode {
tagType: ElementTypes.PORTAL tagType: ElementTypes.PORTAL
codegenNode: ElementCodegenNode | undefined codegenNode: ElementCodegenNode | undefined | CacheExpression
} }
export interface SuspenseNode extends BaseElementNode { export interface SuspenseNode extends BaseElementNode {
tagType: ElementTypes.SUSPENSE tagType: ElementTypes.SUSPENSE
codegenNode: ElementCodegenNode | undefined codegenNode: ElementCodegenNode | undefined | CacheExpression
} }
export interface TextNode extends Node { export interface TextNode extends Node {
@ -298,6 +303,7 @@ export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION type: NodeTypes.JS_CACHE_EXPRESSION
index: number index: number
value: JSChildNode value: JSChildNode
isVNode: boolean
} }
// Codegen Node Types ---------------------------------------------------------- // Codegen Node Types ----------------------------------------------------------
@ -625,12 +631,14 @@ export function createConditionalExpression(
export function createCacheExpression( export function createCacheExpression(
index: number, index: number,
value: JSChildNode value: JSChildNode,
isVNode: boolean = false
): CacheExpression { ): CacheExpression {
return { return {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index, index,
value, value,
isVNode,
loc: locStub loc: locStub
} }
} }

View File

@ -34,7 +34,8 @@ import {
COMMENT, COMMENT,
helperNameMap, helperNameMap,
RESOLVE_COMPONENT, RESOLVE_COMPONENT,
RESOLVE_DIRECTIVE RESOLVE_DIRECTIVE,
SET_BLOCK_TRACKING
} from './runtimeHelpers' } from './runtimeHelpers'
type CodegenNode = TemplateChildNode | JSChildNode type CodegenNode = TemplateChildNode | JSChildNode
@ -247,6 +248,10 @@ export function generate(
.join(', ')} } = _Vue` .join(', ')} } = _Vue`
) )
newline() newline()
if (ast.cached > 0) {
push(`const _cache = $cache`)
newline()
}
newline() newline()
} }
} else { } else {
@ -625,7 +630,22 @@ function genSequenceExpression(
} }
function genCacheExpression(node: CacheExpression, context: CodegenContext) { 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) genNode(node.value, context)
context.push(`)`) if (node.isVNode) {
push(`,`)
newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`)
newline()
push(`_cache[${node.index}]`)
deindent()
}
push(`)`)
} }

View File

@ -44,6 +44,7 @@ export function baseCompile(
...options, ...options,
prefixIdentifiers, prefixIdentifiers,
nodeTransforms: [ nodeTransforms: [
transformOnce,
transformIf, transformIf,
transformFor, transformFor,
...(prefixIdentifiers ...(prefixIdentifiers
@ -62,7 +63,6 @@ export function baseCompile(
directiveTransforms: { directiveTransforms: {
on: transformOn, on: transformOn,
bind: transformBind, bind: transformBind,
once: transformOnce,
model: transformModel, model: transformModel,
...(options.directiveTransforms || {}) // user transforms ...(options.directiveTransforms || {}) // user transforms
} }

View File

@ -19,6 +19,7 @@ export const TO_STRING = Symbol(__DEV__ ? `toString` : ``)
export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``) export const MERGE_PROPS = Symbol(__DEV__ ? `mergeProps` : ``)
export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``) export const TO_HANDLERS = Symbol(__DEV__ ? `toHandlers` : ``)
export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``) 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 // Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime! // generated code. Make sure these are correctly exported in the runtime!
@ -42,7 +43,8 @@ export const helperNameMap: any = {
[TO_STRING]: `toString`, [TO_STRING]: `toString`,
[MERGE_PROPS]: `mergeProps`, [MERGE_PROPS]: `mergeProps`,
[TO_HANDLERS]: `toHandlers`, [TO_HANDLERS]: `toHandlers`,
[CAMELIZE]: `camelize` [CAMELIZE]: `camelize`,
[SET_BLOCK_TRACKING]: `setBlockTracking`
} }
export function registerRuntimeHelpers(helpers: any) { export function registerRuntimeHelpers(helpers: any) {

View File

@ -100,7 +100,7 @@ export interface TransformContext extends Required<TransformOptions> {
addIdentifiers(exp: ExpressionNode | string): void addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: JSChildNode): SimpleExpressionNode hoist(exp: JSChildNode): SimpleExpressionNode
cache<T extends JSChildNode>(exp: T): CacheExpression | T cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
} }
function createTransformContext( function createTransformContext(
@ -219,8 +219,8 @@ function createTransformContext(
true true
) )
}, },
cache(exp) { cache(exp, isVNode = false) {
return cacheHandlers ? createCacheExpression(++context.cached, exp) : exp return createCacheExpression(++context.cached, exp, isVNode)
} }
} }
@ -260,12 +260,17 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
const codegenNode = child.codegenNode as const codegenNode = child.codegenNode as
| ElementCodegenNode | ElementCodegenNode
| ComponentCodegenNode | ComponentCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) { | CacheExpression
codegenNode.arguments[0].callee = helper(CREATE_BLOCK) 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 { } else {
codegenNode.callee = helper(CREATE_BLOCK) root.codegenNode = codegenNode
} }
root.codegenNode = createBlockExpression(codegenNode, context)
} else { } else {
// - single <slot/>, IfNode, ForNode: already blocks. // - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched. // - single text node: always patched.

View File

@ -4,12 +4,12 @@ import {
TemplateChildNode, TemplateChildNode,
SimpleExpressionNode, SimpleExpressionNode,
ElementTypes, ElementTypes,
ElementCodegenNode,
PlainElementNode, PlainElementNode,
ComponentNode, ComponentNode,
TemplateNode, TemplateNode,
ElementNode, ElementNode,
PlainElementCodegenNode PlainElementCodegenNode,
CodegenNodeWithDirective
} from '../ast' } from '../ast'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'
import { WITH_DIRECTIVES } from '../runtimeHelpers' import { WITH_DIRECTIVES } from '../runtimeHelpers'
@ -57,17 +57,20 @@ function walk(
} else { } else {
// node may contain dynamic children, but its props may be eligible for // node may contain dynamic children, but its props may be eligible for
// hoisting. // hoisting.
const flag = getPatchFlag(child) const codegenNode = child.codegenNode!
if ( if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
(!flag || const flag = getPatchFlag(codegenNode)
flag === PatchFlags.NEED_PATCH || if (
flag === PatchFlags.TEXT) && (!flag ||
!hasDynamicKeyOrRef(child) && flag === PatchFlags.NEED_PATCH ||
!hasCachedProps(child) flag === PatchFlags.TEXT) &&
) { !hasDynamicKeyOrRef(child) &&
const props = getNodeProps(child) !hasCachedProps(child)
if (props && props !== `null`) { ) {
getVNodeCall(child).arguments[1] = context.hoist(props) 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) { if (cached !== undefined) {
return cached 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)) { if (!flag && !hasDynamicKeyOrRef(node) && !hasCachedProps(node)) {
// element self is static. check its children. // element self is static. check its children.
for (let i = 0; i < node.children.length; i++) { for (let i = 0; i < node.children.length; i++) {
@ -165,26 +172,32 @@ function hasCachedProps(node: PlainElementNode): boolean {
return false return false
} }
function getVNodeCall(node: PlainElementNode) { function getNodeProps(node: PlainElementNode) {
let codegenNode = node.codegenNode as ElementCodegenNode const codegenNode = node.codegenNode!
if (codegenNode.callee === WITH_DIRECTIVES) { if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
codegenNode = codegenNode.arguments[0] return getVNodeArgAt(
codegenNode,
1
) as PlainElementCodegenNode['arguments'][1]
} }
return codegenNode
} }
type NonCachedCodegenNode =
| PlainElementCodegenNode
| CodegenNodeWithDirective<PlainElementCodegenNode>
function getVNodeArgAt( function getVNodeArgAt(
node: PlainElementNode, node: NonCachedCodegenNode,
index: number index: number
): PlainElementCodegenNode['arguments'][number] { ): PlainElementCodegenNode['arguments'][number] {
return getVNodeCall(node).arguments[index] return getVNodeCall(node).arguments[index]
} }
function getPatchFlag(node: PlainElementNode): number | undefined { function getVNodeCall(node: NonCachedCodegenNode) {
return node.callee === WITH_DIRECTIVES ? node.arguments[0] : node
}
function getPatchFlag(node: NonCachedCodegenNode): number | undefined {
const flag = getVNodeArgAt(node, 3) as string const flag = getVNodeArgAt(node, 3) as string
return flag ? parseInt(flag, 10) : undefined return flag ? parseInt(flag, 10) : undefined
} }
function getNodeProps(node: PlainElementNode) {
return getVNodeArgAt(node, 1) as PlainElementCodegenNode['arguments'][1]
}

View File

@ -280,6 +280,11 @@ export function buildProps(
continue 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 // special case for v-bind and v-on with no argument
const isBind = name === 'bind' const isBind = name === 'bind'
const isOn = name === 'on' const isOn = name === 'on'

View File

@ -15,7 +15,8 @@ import {
createObjectExpression, createObjectExpression,
createObjectProperty, createObjectProperty,
ForCodegenNode, ForCodegenNode,
ElementCodegenNode ElementCodegenNode,
SlotOutletCodegenNode
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
@ -130,7 +131,7 @@ export const transformFor = createStructuralDirectiveTransform(
: null : null
if (slotOutlet) { if (slotOutlet) {
// <slot v-for="..."> or <template v-for="..."><slot/></template> // <slot v-for="..."> or <template v-for="..."><slot/></template>
childBlock = slotOutlet.codegenNode! childBlock = slotOutlet.codegenNode as SlotOutletCodegenNode
if (isTemplate && keyProperty) { if (isTemplate && keyProperty) {
// <template v-for="..." :key="..."><slot/></template> // <template v-for="..." :key="..."><slot/></template>
// we need to inject the key to the renderSlot() call. // we need to inject the key to the renderSlot() call.

View File

@ -69,6 +69,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
if ( if (
!__BROWSER__ && !__BROWSER__ &&
context.prefixIdentifiers && context.prefixIdentifiers &&
context.cacheHandlers &&
!hasScopeRef(exp, context.identifiers) !hasScopeRef(exp, context.identifiers)
) { ) {
props[1].value = context.cache(props[1].value) props[1].value = context.cache(props[1].value)

View File

@ -1,17 +1,15 @@
import { import { NodeTransform } from '../transform'
DirectiveTransform, import { findDir } from '../utils'
createObjectProperty, import { NodeTypes } from '../ast'
createSimpleExpression import { SET_BLOCK_TRACKING } from '../runtimeHelpers'
} from '@vue/compiler-core'
export const transformOnce: DirectiveTransform = dir => { export const transformOnce: NodeTransform = (node, context) => {
return { if (node.type === NodeTypes.ELEMENT && findDir(node, 'once', true)) {
props: [ context.helper(SET_BLOCK_TRACKING)
createObjectProperty( return () => {
createSimpleExpression(`$once`, true, dir.loc), if (node.codegenNode) {
createSimpleExpression('true', false) node.codegenNode = context.cache(node.codegenNode, true /* isVNode */)
) }
], }
needRuntime: false
} }
} }

View File

@ -86,7 +86,7 @@ export interface ComponentInternalInstance {
accessCache: Data | null accessCache: Data | null
// cache for render function values that rely on _ctx but won't need updates // cache for render function values that rely on _ctx but won't need updates
// after initialized (e.g. inline handlers) // after initialized (e.g. inline handlers)
renderCache: Function[] | null renderCache: (Function | VNode)[] | null
components: Record<string, Component> components: Record<string, Component>
directives: Record<string, Directive> directives: Record<string, Directive>

View File

@ -179,14 +179,10 @@ export function createRenderer<
optimized: boolean = false optimized: boolean = false
) { ) {
// patching & not same type, unmount old tree // patching & not same type, unmount old tree
if (n1 != null) { if (n1 != null && !isSameType(n1, n2)) {
if (!isSameType(n1, n2)) { anchor = getNextHostNode(n1)
anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true)
unmount(n1, parentComponent, parentSuspense, true) n1 = null
n1 = null
} else if (n1.props && n1.props.$once) {
return
}
} }
const { type, shapeFlag } = n2 const { type, shapeFlag } = n2

View File

@ -49,6 +49,7 @@ export { toString } from './helpers/toString'
export { toHandlers } from './helpers/toHandlers' export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot' export { renderSlot } from './helpers/renderSlot'
export { createSlots } from './helpers/createSlots' export { createSlots } from './helpers/createSlots'
export { setBlockTracking } from './vnode'
export { capitalize, camelize } from '@vue/shared' export { capitalize, camelize } from '@vue/shared'
// Internal, for integration with runtime compiler // Internal, for integration with runtime compiler

View File

@ -105,7 +105,24 @@ export function openBlock(disableTracking?: boolean) {
blockStack.push((currentBlock = disableTracking ? null : [])) 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`. // 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 // A block root keeps track of dynamic nodes within the block in the
@ -118,9 +135,9 @@ export function createBlock(
dynamicProps?: string[] dynamicProps?: string[]
): VNode { ): VNode {
// avoid a block with patchFlag tracking itself // avoid a block with patchFlag tracking itself
shouldTrack = false shouldTrack--
const vnode = createVNode(type, props, children, patchFlag, dynamicProps) const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
shouldTrack = true shouldTrack++
// save current block children on the block vnode // save current block children on the block vnode
vnode.dynamicChildren = currentBlock || EMPTY_ARR vnode.dynamicChildren = currentBlock || EMPTY_ARR
// close block // close block
@ -200,7 +217,7 @@ export function createVNode(
// component doesn't need to update, it needs to persist the instance on to // 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. // the next vnode so that it can be properly unmounted later.
if ( if (
shouldTrack && shouldTrack > 0 &&
currentBlock !== null && currentBlock !== null &&
(patchFlag > 0 || (patchFlag > 0 ||
shapeFlag & ShapeFlags.STATEFUL_COMPONENT || shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||

View File

@ -52,7 +52,7 @@ export const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]' toTypeString(val) === '[object Object]'
export const isReservedProp = (key: string): boolean => 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 const camelizeRE = /-(\w)/g
export const camelize = (str: string): string => { export const camelize = (str: string): string => {