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`] = `
"
return function render() {

View 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()
})
})

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" />', {
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: [

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 { 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
expect(codegenArgs[1]).toMatchObject(
createObjectMatcher({
$once: `[true]`
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()
})
// 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 {
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))

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

View 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(`)`)
}

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

View 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) {

View 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
| 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 {
root.codegenNode = codegenNode
}
} else {
// - single <slot/>, IfNode, ForNode: already blocks.
// - single text node: always patched.

View 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,7 +57,9 @@ function walk(
} else {
// node may contain dynamic children, but its props may be eligible for
// hoisting.
const flag = getPatchFlag(child)
const codegenNode = child.codegenNode!
if (codegenNode.type === NodeTypes.JS_CALL_EXPRESSION) {
const flag = getPatchFlag(codegenNode)
if (
(!flag ||
flag === PatchFlags.NEED_PATCH ||
@ -67,7 +69,8 @@ function walk(
) {
const props = getNodeProps(child)
if (props && props !== `null`) {
getVNodeCall(child).arguments[1] = context.hoist(props)
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 {
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
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
}
// 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'

View 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.

View 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)

View 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 */)
}
}
}
}

View 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>

View 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)) {
if (n1 != null && !isSameType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
} else if (n1.props && n1.props.$once) {
return
}
}
const { type, shapeFlag } = n2

View 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

View 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 ||

View 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 => {