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

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