feat(v-on): cache handlers

This commit is contained in:
Evan You
2019-10-18 21:51:34 -04:00
parent 39ea67a2d2
commit 58593c4714
19 changed files with 529 additions and 243 deletions

View File

@@ -42,7 +42,8 @@ export const enum NodeTypes {
JS_ARRAY_EXPRESSION,
JS_FUNCTION_EXPRESSION,
JS_SEQUENCE_EXPRESSION,
JS_CONDITIONAL_EXPRESSION
JS_CONDITIONAL_EXPRESSION,
JS_CACHE_EXPRESSION
}
export const enum ElementTypes {
@@ -93,6 +94,7 @@ export interface RootNode extends Node {
components: string[]
directives: string[]
hoists: JSChildNode[]
cached: number
codegenNode: TemplateChildNode | JSChildNode | undefined
}
@@ -236,6 +238,7 @@ export type JSChildNode =
| FunctionExpression
| ConditionalExpression
| SequenceExpression
| CacheExpression
export interface CallExpression extends Node {
type: NodeTypes.JS_CALL_EXPRESSION
@@ -283,6 +286,12 @@ export interface ConditionalExpression extends Node {
alternate: JSChildNode
}
export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION
index: number
value: JSChildNode
}
// Codegen Node Types ----------------------------------------------------------
// createVNode(...)
@@ -605,3 +614,15 @@ export function createConditionalExpression(
loc: locStub
}
}
export function createCacheExpression(
index: number,
value: JSChildNode
): CacheExpression {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
index,
value,
loc: locStub
}
}

View File

@@ -16,7 +16,8 @@ import {
SimpleExpressionNode,
FunctionExpression,
SequenceExpression,
ConditionalExpression
ConditionalExpression,
CacheExpression
} from './ast'
import { SourceMapGenerator, RawSourceMap } from 'source-map'
import {
@@ -218,6 +219,7 @@ export function generate(
}
}
genHoists(ast.hoists, context)
genCached(ast.cached, context)
newline()
push(`return `)
} else {
@@ -226,6 +228,7 @@ export function generate(
push(`import { ${ast.helpers.map(helper).join(', ')} } from "vue"\n`)
}
genHoists(ast.hoists, context)
genCached(ast.cached, context)
newline()
push(`export default `)
}
@@ -315,6 +318,18 @@ function genHoists(hoists: JSChildNode[], context: CodegenContext) {
})
}
function genCached(cached: number, context: CodegenContext) {
if (cached > 0) {
context.newline()
context.push(`let `)
for (let i = 0; i < cached; i++) {
context.push(`_cached_${i + 1}`)
if (i !== cached - 1) context.push(`, `)
}
context.newline()
}
}
function isText(n: string | CodegenNode) {
return (
isString(n) ||
@@ -419,6 +434,9 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
/* istanbul ignore next */
default:
if (__DEV__) {
@@ -612,3 +630,9 @@ function genSequenceExpression(
genNodeList(node.expressions, context)
context.push(`)`)
}
function genCacheExpression(node: CacheExpression, context: CodegenContext) {
context.push(`_cached_${node.index} || (_cached_${node.index} = `)
genNode(node.value, context)
context.push(`)`)
}

View File

@@ -13,7 +13,9 @@ import {
ElementTypes,
ElementCodegenNode,
ComponentCodegenNode,
createCallExpression
createCallExpression,
CacheExpression,
createCacheExpression
} from './ast'
import { isString, isArray } from '@vue/shared'
import { CompilerError, defaultOnError } from './errors'
@@ -45,8 +47,13 @@ export type NodeTransform = (
export type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext
) => {
context: TransformContext,
// a platform specific compiler can import the base transform and augment
// it by passing in this optional argument.
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult
) => DirectiveTransformResult
export interface DirectiveTransformResult {
props: Property[]
needRuntime: boolean | symbol
}
@@ -64,6 +71,7 @@ export interface TransformOptions {
directiveTransforms?: { [name: string]: DirectiveTransform }
prefixIdentifiers?: boolean
hoistStatic?: boolean
cacheHandlers?: boolean
onError?: (error: CompilerError) => void
}
@@ -73,6 +81,7 @@ export interface TransformContext extends Required<TransformOptions> {
components: Set<string>
directives: Set<string>
hoists: JSChildNode[]
cached: number
identifiers: { [name: string]: number | undefined }
scopes: {
vFor: number
@@ -91,6 +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
}
function createTransformContext(
@@ -98,6 +108,7 @@ function createTransformContext(
{
prefixIdentifiers = false,
hoistStatic = false,
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
onError = defaultOnError
@@ -109,6 +120,7 @@ function createTransformContext(
components: new Set(),
directives: new Set(),
hoists: [],
cached: 0,
identifiers: {},
scopes: {
vFor: 0,
@@ -118,6 +130,7 @@ function createTransformContext(
},
prefixIdentifiers,
hoistStatic,
cacheHandlers,
nodeTransforms,
directiveTransforms,
onError,
@@ -204,6 +217,14 @@ function createTransformContext(
false,
exp.loc
)
},
cache(exp) {
if (cacheHandlers) {
context.cached++
return createCacheExpression(context.cached, exp)
} else {
return exp
}
}
}
@@ -273,6 +294,7 @@ function finalizeRoot(root: RootNode, context: TransformContext) {
root.components = [...context.components]
root.directives = [...context.directives]
root.hoists = context.hoists
root.cached = context.cached
}
export function traverseChildren(

View File

@@ -8,17 +8,14 @@ import {
PlainElementNode,
ComponentNode,
TemplateNode,
ElementNode
ElementNode,
PlainElementCodegenNode
} from '../ast'
import { TransformContext } from '../transform'
import { WITH_DIRECTIVES } from '../runtimeHelpers'
import { PatchFlags, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet, findProp } from '../utils'
function hasDynamicKeyOrRef(node: ElementNode) {
return findProp(node, 'key', true) || findProp(node, 'ref', true)
}
export function hoistStatic(root: RootNode, context: TransformContext) {
walk(
root.children,
@@ -53,10 +50,11 @@ function walk(
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
const hasBailoutProp = hasDynamicKeyOrRef(child) || hasCachedProps(child)
if (
!doNotHoistNode &&
isStaticNode(child, resultCache) &&
!hasDynamicKeyOrRef(child)
!hasBailoutProp &&
isStaticNode(child, resultCache)
) {
// whole tree is static
child.codegenNode = context.hoist(child.codegenNode!)
@@ -69,15 +67,11 @@ function walk(
(!flag ||
flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) &&
!hasDynamicKeyOrRef(child)
!hasBailoutProp
) {
let codegenNode = child.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
const props = codegenNode.arguments[1]
const props = getNodeProps(child)
if (props && props !== `null`) {
codegenNode.arguments[1] = context.hoist(props)
getVNodeCall(child).arguments[1] = context.hoist(props)
}
}
}
@@ -97,15 +91,6 @@ function walk(
}
}
function getPatchFlag(node: PlainElementNode): number | undefined {
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
const flag = codegenNode.arguments[3]
return flag ? parseInt(flag, 10) : undefined
}
export function isStaticNode(
node: TemplateChildNode | SimpleExpressionNode,
resultCache: Map<TemplateChildNode, boolean> = new Map()
@@ -157,3 +142,51 @@ export function isStaticNode(
return false
}
}
function hasDynamicKeyOrRef(node: ElementNode): boolean {
return !!(findProp(node, 'key', true) || findProp(node, 'ref', true))
}
function hasCachedProps(node: PlainElementNode): boolean {
if (__BROWSER__) {
return false
}
const props = getNodeProps(node)
if (
props &&
props !== 'null' &&
props.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
const { properties } = props
for (let i = 0; i < properties.length; i++) {
if (properties[i].value.type === NodeTypes.JS_CACHE_EXPRESSION) {
return true
}
}
}
return false
}
function getVNodeCall(node: PlainElementNode) {
let codegenNode = node.codegenNode as ElementCodegenNode
if (codegenNode.callee === WITH_DIRECTIVES) {
codegenNode = codegenNode.arguments[0]
}
return codegenNode
}
function getVNodeArgAt(
node: PlainElementNode,
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 getNodeProps(node: PlainElementNode) {
return getVNodeArgAt(node, 1) as PlainElementCodegenNode['arguments'][1]
}

View File

@@ -222,9 +222,10 @@ export function buildProps(
const analyzePatchFlag = ({ key, value }: Property) => {
if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
if (
(value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.JS_CACHE_EXPRESSION ||
((value.type === NodeTypes.SIMPLE_EXPRESSION ||
value.type === NodeTypes.COMPOUND_EXPRESSION) &&
isStaticNode(value)
isStaticNode(value))
) {
return
}

View File

@@ -1,17 +1,14 @@
import { DirectiveTransform, TransformContext } from '../transform'
import { DirectiveTransform } from '../transform'
import {
createSimpleExpression,
createObjectProperty,
createCompoundExpression,
NodeTypes,
Property,
CompoundExpressionNode,
createInterpolation,
ElementTypes
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { isMemberExpression, isSimpleIdentifier } from '../utils'
import { isObject } from '@vue/shared'
import { isMemberExpression, isSimpleIdentifier, hasScopeRef } from '../utils'
export const transformModel: DirectiveTransform = (dir, node, context) => {
const { exp, arg } = dir
@@ -54,16 +51,6 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
])
: createSimpleExpression('onUpdate:modelValue', true)
let assignmentChildren =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children
// For a member expression used in assignment, it only needs to be updated
// if the expression involves scope variables. Otherwise we can mark the
// expression as constant to avoid it being included in `dynamicPropNames`
// of the element. This optimization relies on `prefixIdentifiers: true`.
if (!__BROWSER__ && context.prefixIdentifiers) {
assignmentChildren = assignmentChildren.map(c => toConstant(c, context))
}
const props = [
// modelValue: foo
createObjectProperty(propName, dir.exp!),
@@ -72,12 +59,21 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
eventName,
createCompoundExpression([
`$event => (`,
...assignmentChildren,
...(exp.type === NodeTypes.SIMPLE_EXPRESSION ? [exp] : exp.children),
` = $event)`
])
)
]
// cache v-model handler if applicable (when it doesn't refer any scope vars)
if (
!__BROWSER__ &&
context.prefixIdentifiers &&
!hasScopeRef(exp, context.identifiers)
) {
props[1].value = context.cache(props[1].value)
}
// modelModifiers: { foo: true, "bar-baz": true }
if (dir.modifiers.length && node.tagType === ElementTypes.COMPONENT) {
const modifiers = dir.modifiers
@@ -94,30 +90,6 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
return createTransformProps(props)
}
function toConstant(
exp: CompoundExpressionNode | CompoundExpressionNode['children'][0],
context: TransformContext
): any {
if (!isObject(exp) || exp.type === NodeTypes.TEXT) {
return exp
}
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
if (exp.isStatic || context.identifiers[exp.content]) {
return exp
}
return {
...exp,
isConstant: true
}
} else if (exp.type === NodeTypes.COMPOUND_EXPRESSION) {
return createCompoundExpression(
exp.children.map(c => toConstant(c, context))
)
} else if (exp.type === NodeTypes.INTERPOLATION) {
return createInterpolation(toConstant(exp.content, context), exp.loc)
}
}
function createTransformProps(props: Property[] = []) {
return { props, needRuntime: false }
}

View File

@@ -1,4 +1,4 @@
import { DirectiveTransform } from '../transform'
import { DirectiveTransform, DirectiveTransformResult } from '../transform'
import {
DirectiveNode,
createObjectProperty,
@@ -11,7 +11,7 @@ import {
import { capitalize } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
import { isMemberExpression } from '../utils'
import { isMemberExpression, hasScopeRef } from '../utils'
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
@@ -28,7 +28,8 @@ export interface VOnDirectiveNode extends DirectiveNode {
export const transformOn: DirectiveTransform = (
dir: VOnDirectiveNode,
node,
context
context,
augmentor
) => {
const { loc, modifiers, arg } = dir
if (!dir.exp && !modifiers.length) {
@@ -51,22 +52,37 @@ export const transformOn: DirectiveTransform = (
eventName.children.unshift(`"on" + (`)
eventName.children.push(`)`)
}
// TODO .once modifier handling since it is platform agnostic
// other modifiers are handled in compiler-dom
// handler processing
let exp: ExpressionNode | undefined = dir.exp
let isCacheable: boolean = !exp
if (exp) {
const isInlineStatement = !(
isMemberExpression(exp.content) || fnExpRE.test(exp.content)
)
const isMemberExp = isMemberExpression(exp.content)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
// process the expression since it's been skipped
if (!__BROWSER__ && context.prefixIdentifiers) {
context.addIdentifiers(`$event`)
exp = processExpression(exp, context)
context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference
// to scope variables.
isCacheable =
context.cacheHandlers && !hasScopeRef(exp, context.identifiers)
// If the expression is optimizable and is a member expression pointing
// to a function, turn it into invocation (and wrap in an arrow function
// below) so that it always accesses the latest value when called - thus
// avoiding the need to be patched.
if (isCacheable && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content += `($event)`
} else {
exp.children.push(`($event)`)
}
}
}
if (isInlineStatement) {
if (isInlineStatement || (isCacheable && isMemberExp)) {
// wrap inline statement in a function expression
exp = createCompoundExpression([
`$event => (`,
@@ -76,7 +92,7 @@ export const transformOn: DirectiveTransform = (
}
}
return {
let ret: DirectiveTransformResult = {
props: [
createObjectProperty(
eventName,
@@ -85,4 +101,18 @@ export const transformOn: DirectiveTransform = (
],
needRuntime: false
}
// apply extended compiler augmentor
if (augmentor) {
ret = augmentor(ret)
}
if (isCacheable) {
// cache handlers so that it's always the same handler being passed down.
// this avoids unnecessary re-renders when users use inline hanlders on
// components.
ret.props[0].value = context.cache(ret.props[0].value)
}
return ret
}

View File

@@ -19,21 +19,13 @@ import {
FunctionExpression,
CallExpression,
createCallExpression,
createArrayExpression,
IfBranchNode
createArrayExpression
} from '../ast'
import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors'
import {
findDir,
isTemplateNode,
assert,
isVSlot,
isSimpleIdentifier
} from '../utils'
import { findDir, isTemplateNode, assert, isVSlot, hasScopeRef } from '../utils'
import { CREATE_SLOTS, RENDER_LIST } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor'
import { isObject } from '@vue/shared'
const isStaticExp = (p: JSChildNode): p is SimpleExpressionNode =>
p.type === NodeTypes.SIMPLE_EXPRESSION && p.isStatic
@@ -337,49 +329,3 @@ function buildDynamicSlot(
createObjectProperty(`fn`, fn)
])
}
function hasScopeRef(
node: TemplateChildNode | IfBranchNode | SimpleExpressionNode | undefined,
ids: TransformContext['identifiers']
): boolean {
if (!node || Object.keys(ids).length === 0) {
return false
}
switch (node.type) {
case NodeTypes.ELEMENT:
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
(hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
) {
return true
}
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.FOR:
if (hasScopeRef(node.source, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.IF:
return node.branches.some(b => hasScopeRef(b, ids))
case NodeTypes.IF_BRANCH:
if (hasScopeRef(node.condition, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.SIMPLE_EXPRESSION:
return (
!node.isStatic &&
isSimpleIdentifier(node.content) &&
!!ids[node.content]
)
case NodeTypes.COMPOUND_EXPRESSION:
return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
case NodeTypes.INTERPOLATION:
return hasScopeRef(node.content, ids)
default:
return false
}
}

View File

@@ -21,13 +21,14 @@ import {
ElementCodegenNode,
SlotOutletCodegenNode,
ComponentCodegenNode,
ExpressionNode
ExpressionNode,
IfBranchNode
} from './ast'
import { parse } from 'acorn'
import { walk } from 'estree-walker'
import { TransformContext } from './transform'
import { OPEN_BLOCK, MERGE_PROPS, RENDER_SLOT } from './runtimeHelpers'
import { isString, isFunction } from '@vue/shared'
import { isString, isFunction, isObject } from '@vue/shared'
// cache node requires
// lazy require dependencies so that they don't end up in rollup's dep graph
@@ -250,3 +251,51 @@ export function toValidAssetId(
export function isEmptyExpression(node: ExpressionNode) {
return node.type === NodeTypes.SIMPLE_EXPRESSION && !node.content.trim()
}
// Check if a node contains expressions that reference current context scope ids
export function hasScopeRef(
node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined,
ids: TransformContext['identifiers']
): boolean {
if (!node || Object.keys(ids).length === 0) {
return false
}
switch (node.type) {
case NodeTypes.ELEMENT:
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (
p.type === NodeTypes.DIRECTIVE &&
(hasScopeRef(p.arg, ids) || hasScopeRef(p.exp, ids))
) {
return true
}
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.FOR:
if (hasScopeRef(node.source, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.IF:
return node.branches.some(b => hasScopeRef(b, ids))
case NodeTypes.IF_BRANCH:
if (hasScopeRef(node.condition, ids)) {
return true
}
return node.children.some(c => hasScopeRef(c, ids))
case NodeTypes.SIMPLE_EXPRESSION:
return (
!node.isStatic &&
isSimpleIdentifier(node.content) &&
!!ids[node.content]
)
case NodeTypes.COMPOUND_EXPRESSION:
return node.children.some(c => isObject(c) && hasScopeRef(c, ids))
case NodeTypes.INTERPOLATION:
return hasScopeRef(node.content, ids)
default:
// TextNode or CommentNode
return false
}
}