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

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

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