wip: further optimize bindings

This commit is contained in:
Evan You 2020-11-12 16:11:14 -05:00
parent 6a9b56ca60
commit 128621d6a0
7 changed files with 110 additions and 58 deletions

View File

@ -61,8 +61,16 @@ export type HoistTransform = (
parent: ParentNode parent: ParentNode
) => void ) => void
export const enum BindingTypes {
DATA = 'data',
PROPS = 'props',
SETUP = 'setup',
CONST = 'const',
OPTIONS = 'options'
}
export interface BindingMetadata { export interface BindingMetadata {
[key: string]: 'data' | 'props' | 'setup' | 'options' | 'setup-raw' [key: string]: BindingTypes
} }
interface SharedTransformCodegenOptions { interface SharedTransformCodegenOptions {

View File

@ -207,10 +207,10 @@ export function getStaticType(
case NodeTypes.TEXT_CALL: case NodeTypes.TEXT_CALL:
return getStaticType(node.content, resultCache) return getStaticType(node.content, resultCache)
case NodeTypes.SIMPLE_EXPRESSION: case NodeTypes.SIMPLE_EXPRESSION:
return node.isConstant return node.isRuntimeConstant
? node.isRuntimeConstant
? StaticType.HAS_RUNTIME_CONSTANT ? StaticType.HAS_RUNTIME_CONSTANT
: StaticType.FULL_STATIC : node.isConstant
? StaticType.FULL_STATIC
: StaticType.NOT_STATIC : StaticType.NOT_STATIC
case NodeTypes.COMPOUND_EXPRESSION: case NodeTypes.COMPOUND_EXPRESSION:
let returnType = StaticType.FULL_STATIC let returnType = StaticType.FULL_STATIC

View File

@ -54,7 +54,7 @@ import {
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { getStaticType } from './hoistStatic' import { getStaticType } from './hoistStatic'
import { BindingMetadata } from '../options' import { BindingTypes } from '../options'
// some directive transforms (e.g. v-model) may return a symbol for runtime // some directive transforms (e.g. v-model) may return a symbol for runtime
// import, which should be used instead of a resolveDirective call. // import, which should be used instead of a resolveDirective call.
@ -253,7 +253,7 @@ export function resolveComponentType(
// 3. user component (from setup bindings) // 3. user component (from setup bindings)
const bindings = context.bindingMetadata const bindings = context.bindingMetadata
if (bindings !== EMPTY_OBJ) { if (bindings !== EMPTY_OBJ) {
const checkType = (type: BindingMetadata[string]) => { const checkType = (type: BindingTypes) => {
let resolvedTag = tag let resolvedTag = tag
if ( if (
bindings[resolvedTag] === type || bindings[resolvedTag] === type ||
@ -263,17 +263,17 @@ export function resolveComponentType(
return resolvedTag return resolvedTag
} }
} }
const tagFromSetup = checkType('setup') const tagFromSetup = checkType(BindingTypes.SETUP)
if (tagFromSetup) { if (tagFromSetup) {
return context.inline return context.inline
? // setup scope bindings may be refs so they need to be unrefed ? // setup scope bindings may be refs so they need to be unrefed
`${context.helperString(UNREF)}(${tagFromSetup})` `${context.helperString(UNREF)}(${tagFromSetup})`
: `$setup[${JSON.stringify(tagFromSetup)}]` : `$setup[${JSON.stringify(tagFromSetup)}]`
} }
const tagFromImport = checkType('setup-raw') const tagFromConst = checkType(BindingTypes.CONST)
if (tagFromImport) { if (tagFromConst) {
// raw setup bindings (e.g. imports) can be used as-is // constant setup bindings (e.g. imports) can be used as-is
return tagFromImport return tagFromConst
} }
} }

View File

@ -29,6 +29,7 @@ import { validateBrowserExpression } from '../validateExpression'
import { parse } from '@babel/parser' import { parse } from '@babel/parser'
import { walk } from 'estree-walker' import { walk } from 'estree-walker'
import { UNREF } from '../runtimeHelpers' import { UNREF } from '../runtimeHelpers'
import { BindingTypes } from '../options'
const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this') const isLiteralWhitelisted = /*#__PURE__*/ makeMap('true,false,null,this')
@ -99,18 +100,26 @@ export function processExpression(
} }
const { inline, bindingMetadata } = context const { inline, bindingMetadata } = context
// const bindings exposed from setup - we know they never change
if (inline && bindingMetadata[node.content] === BindingTypes.CONST) {
node.isRuntimeConstant = true
return node
}
const prefix = (raw: string) => { const prefix = (raw: string) => {
const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw] const type = hasOwn(bindingMetadata, raw) && bindingMetadata[raw]
if (type === BindingTypes.CONST) {
return raw
}
if (inline) { if (inline) {
// setup inline mode // setup inline mode
if (type === 'setup') { if (type === BindingTypes.SETUP) {
return `${context.helperString(UNREF)}(${raw})` return `${context.helperString(UNREF)}(${raw})`
} else if (type === 'props') { } else if (type === BindingTypes.PROPS) {
// use __props which is generated by compileScript so in ts mode // use __props which is generated by compileScript so in ts mode
// it gets correct type // it gets correct type
return `__props.${raw}` return `__props.${raw}`
} else if (type === 'setup-raw') {
return raw
} }
} }
// fallback to normal // fallback to normal

View File

@ -70,7 +70,7 @@ export const transformOn: DirectiveTransform = (
if (exp && !exp.content.trim()) { if (exp && !exp.content.trim()) {
exp = undefined exp = undefined
} }
let isCacheable: boolean = context.cacheHandlers && !exp let shouldCache: boolean = context.cacheHandlers && !exp
if (exp) { if (exp) {
const isMemberExp = isMemberExpression(exp.content) const isMemberExp = isMemberExpression(exp.content)
const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content)) const isInlineStatement = !(isMemberExp || fnExpRE.test(exp.content))
@ -83,8 +83,11 @@ export const transformOn: DirectiveTransform = (
isInlineStatement && context.removeIdentifiers(`$event`) isInlineStatement && context.removeIdentifiers(`$event`)
// with scope analysis, the function is hoistable if it has no reference // with scope analysis, the function is hoistable if it has no reference
// to scope variables. // to scope variables.
isCacheable = shouldCache =
context.cacheHandlers && context.cacheHandlers &&
// runtime constants don't need to be cached
// (this is analyzed by compileScript in SFC <script setup>)
!(exp.type === NodeTypes.SIMPLE_EXPRESSION && exp.isRuntimeConstant) &&
// #1541 bail if this is a member exp handler passed to a component - // #1541 bail if this is a member exp handler passed to a component -
// we need to use the original function to preserve arity, // we need to use the original function to preserve arity,
// e.g. <transition> relies on checking cb.length to determine // e.g. <transition> relies on checking cb.length to determine
@ -98,7 +101,7 @@ export const transformOn: DirectiveTransform = (
// to a function, turn it into invocation (and wrap in an arrow function // 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 // below) so that it always accesses the latest value when called - thus
// avoiding the need to be patched. // avoiding the need to be patched.
if (isCacheable && isMemberExp) { if (shouldCache && isMemberExp) {
if (exp.type === NodeTypes.SIMPLE_EXPRESSION) { if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
exp.content += `(...args)` exp.content += `(...args)`
} else { } else {
@ -116,7 +119,7 @@ export const transformOn: DirectiveTransform = (
) )
} }
if (isInlineStatement || (isCacheable && isMemberExp)) { if (isInlineStatement || (shouldCache && isMemberExp)) {
// wrap inline statement in a function expression // wrap inline statement in a function expression
exp = createCompoundExpression([ exp = createCompoundExpression([
`${isInlineStatement ? `$event` : `(...args)`} => ${ `${isInlineStatement ? `$event` : `(...args)`} => ${
@ -142,7 +145,7 @@ export const transformOn: DirectiveTransform = (
ret = augmentor(ret) ret = augmentor(ret)
} }
if (isCacheable) { if (shouldCache) {
// cache handlers so that it's always the same handler being passed down. // cache handlers so that it's always the same handler being passed down.
// this avoids unnecessary re-renders when users use inline handlers on // this avoids unnecessary re-renders when users use inline handlers on
// components. // components.

View File

@ -26,6 +26,7 @@ import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map' import { RawSourceMap } from 'source-map'
import { genCssVarsCode, injectCssVarsCalls } from './genCssVars' import { genCssVarsCode, injectCssVarsCalls } from './genCssVars'
import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate' import { compileTemplate, SFCTemplateCompileOptions } from './compileTemplate'
import { BindingTypes } from 'packages/compiler-core/src/options'
const CTX_FN_NAME = 'defineContext' const CTX_FN_NAME = 'defineContext'
@ -134,8 +135,11 @@ export function compileScript(
source: string source: string
} }
> = Object.create(null) > = Object.create(null)
const setupBindings: Record<string, 'var' | 'const'> = Object.create(null) const setupBindings: Record<
const refBindings: Record<string, 'var'> = Object.create(null) string,
BindingTypes.SETUP | BindingTypes.CONST
> = Object.create(null)
const refBindings: Record<string, BindingTypes.SETUP> = Object.create(null)
const refIdentifiers: Set<Identifier> = new Set() const refIdentifiers: Set<Identifier> = new Set()
const enableRefSugar = options.refSugar !== false const enableRefSugar = options.refSugar !== false
let defaultExport: Node | undefined let defaultExport: Node | undefined
@ -222,7 +226,7 @@ export function compileScript(
if (id.name[0] === '$') { if (id.name[0] === '$') {
error(`ref variable identifiers cannot start with $.`, id) error(`ref variable identifiers cannot start with $.`, id)
} }
refBindings[id.name] = setupBindings[id.name] = 'var' refBindings[id.name] = setupBindings[id.name] = BindingTypes.SETUP
refIdentifiers.add(id) refIdentifiers.add(id)
} }
@ -712,9 +716,9 @@ export function compileScript(
}` }`
} }
const allBindings = { ...setupBindings } const allBindings: Record<string, any> = { ...setupBindings }
for (const key in userImports) { for (const key in userImports) {
allBindings[key] = 'var' allBindings[key] = true
} }
// 9. inject `useCssVars` calls // 9. inject `useCssVars` calls
@ -737,7 +741,7 @@ export function compileScript(
} }
if (setupContextType) { if (setupContextType) {
for (const key in typeDeclaredProps) { for (const key in typeDeclaredProps) {
bindingMetadata[key] = 'props' bindingMetadata[key] = BindingTypes.PROPS
} }
} }
if (setupContextArg) { if (setupContextArg) {
@ -745,15 +749,16 @@ export function compileScript(
} }
if (options.inlineTemplate) { if (options.inlineTemplate) {
for (const [key, { source }] of Object.entries(userImports)) { for (const [key, { source }] of Object.entries(userImports)) {
bindingMetadata[key] = source.endsWith('.vue') ? 'setup-raw' : 'setup' bindingMetadata[key] = source.endsWith('.vue')
? BindingTypes.CONST
: BindingTypes.SETUP
} }
for (const key in setupBindings) { for (const key in setupBindings) {
bindingMetadata[key] = bindingMetadata[key] = setupBindings[key]
setupBindings[key] === 'var' ? 'setup' : 'setup-raw'
} }
} else { } else {
for (const key in allBindings) { for (const key in allBindings) {
bindingMetadata[key] = 'setup' bindingMetadata[key] = BindingTypes.SETUP
} }
} }
@ -867,25 +872,36 @@ export function compileScript(
} }
} }
function walkDeclaration(node: Declaration, bindings: Record<string, string>) { function walkDeclaration(
node: Declaration,
bindings: Record<string, BindingTypes>
) {
if (node.type === 'VariableDeclaration') { if (node.type === 'VariableDeclaration') {
const isConst = node.kind === 'const' const isConst = node.kind === 'const'
// export const foo = ... // export const foo = ...
for (const { id, init } of node.declarations) { for (const { id, init } of node.declarations) {
const isContextCall = !!(
isConst &&
init &&
init.type === 'CallExpression' &&
init.callee.type === 'Identifier' &&
init.callee.name === CTX_FN_NAME
)
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
bindings[id.name] = bindings[id.name] =
// if a declaration is a const literal, we can mark it so that // if a declaration is a const literal, we can mark it so that
// the generated render fn code doesn't need to unref() it // the generated render fn code doesn't need to unref() it
isConst && isContextCall ||
(isConst &&
init!.type !== 'Identifier' && // const a = b init!.type !== 'Identifier' && // const a = b
init!.type !== 'CallExpression' && // const a = ref() init!.type !== 'CallExpression' && // const a = ref()
init!.type !== 'MemberExpression' // const a = b.c init!.type !== 'MemberExpression') // const a = b.c
? 'const' ? BindingTypes.CONST
: 'var' : BindingTypes.SETUP
} else if (id.type === 'ObjectPattern') { } else if (id.type === 'ObjectPattern') {
walkObjectPattern(id, bindings, isConst) walkObjectPattern(id, bindings, isConst, isContextCall)
} else if (id.type === 'ArrayPattern') { } else if (id.type === 'ArrayPattern') {
walkArrayPattern(id, bindings, isConst) walkArrayPattern(id, bindings, isConst, isContextCall)
} }
} }
} else if ( } else if (
@ -894,14 +910,15 @@ function walkDeclaration(node: Declaration, bindings: Record<string, string>) {
) { ) {
// export function foo() {} / export class Foo {} // export function foo() {} / export class Foo {}
// export declarations must be named. // export declarations must be named.
bindings[node.id!.name] = 'const' bindings[node.id!.name] = BindingTypes.CONST
} }
} }
function walkObjectPattern( function walkObjectPattern(
node: ObjectPattern, node: ObjectPattern,
bindings: Record<string, string>, bindings: Record<string, BindingTypes>,
isConst: boolean isConst: boolean,
isContextCall = false
) { ) {
for (const p of node.properties) { for (const p of node.properties) {
if (p.type === 'ObjectProperty') { if (p.type === 'ObjectProperty') {
@ -909,46 +926,58 @@ function walkObjectPattern(
if (p.key.type === 'Identifier') { if (p.key.type === 'Identifier') {
if (p.key === p.value) { if (p.key === p.value) {
// const { x } = ... // const { x } = ...
bindings[p.key.name] = 'var' bindings[p.key.name] = isContextCall
? BindingTypes.CONST
: BindingTypes.SETUP
} else { } else {
walkPattern(p.value, bindings, isConst) walkPattern(p.value, bindings, isConst, isContextCall)
} }
} }
} else { } else {
// ...rest // ...rest
// argument can only be identifer when destructuring // argument can only be identifer when destructuring
bindings[(p.argument as Identifier).name] = isConst ? 'const' : 'var' bindings[(p.argument as Identifier).name] = isConst
? BindingTypes.CONST
: BindingTypes.SETUP
} }
} }
} }
function walkArrayPattern( function walkArrayPattern(
node: ArrayPattern, node: ArrayPattern,
bindings: Record<string, string>, bindings: Record<string, BindingTypes>,
isConst: boolean isConst: boolean,
isContextCall = false
) { ) {
for (const e of node.elements) { for (const e of node.elements) {
e && walkPattern(e, bindings, isConst) e && walkPattern(e, bindings, isConst, isContextCall)
} }
} }
function walkPattern( function walkPattern(
node: Node, node: Node,
bindings: Record<string, string>, bindings: Record<string, BindingTypes>,
isConst: boolean isConst: boolean,
isContextCall = false
) { ) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
bindings[node.name] = 'var' bindings[node.name] = isContextCall
? BindingTypes.CONST
: BindingTypes.SETUP
} else if (node.type === 'RestElement') { } else if (node.type === 'RestElement') {
// argument can only be identifer when destructuring // argument can only be identifer when destructuring
bindings[(node.argument as Identifier).name] = isConst ? 'const' : 'var' bindings[(node.argument as Identifier).name] = isConst
? BindingTypes.CONST
: BindingTypes.SETUP
} else if (node.type === 'ObjectPattern') { } else if (node.type === 'ObjectPattern') {
walkObjectPattern(node, bindings, isConst) walkObjectPattern(node, bindings, isConst)
} else if (node.type === 'ArrayPattern') { } else if (node.type === 'ArrayPattern') {
walkArrayPattern(node, bindings, isConst) walkArrayPattern(node, bindings, isConst)
} else if (node.type === 'AssignmentPattern') { } else if (node.type === 'AssignmentPattern') {
if (node.left.type === 'Identifier') { if (node.left.type === 'Identifier') {
bindings[node.left.name] = 'var' bindings[node.left.name] = isContextCall
? BindingTypes.CONST
: BindingTypes.SETUP
} else { } else {
walkPattern(node.left, bindings, isConst) walkPattern(node.left, bindings, isConst)
} }
@ -1336,7 +1365,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
// props: ['foo'] // props: ['foo']
// props: { foo: ... } // props: { foo: ... }
for (const key of getObjectOrArrayExpressionKeys(property)) { for (const key of getObjectOrArrayExpressionKeys(property)) {
bindings[key] = 'props' bindings[key] = BindingTypes.PROPS
} }
} }
@ -1345,7 +1374,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
// inject: ['foo'] // inject: ['foo']
// inject: { foo: {} } // inject: { foo: {} }
for (const key of getObjectOrArrayExpressionKeys(property)) { for (const key of getObjectOrArrayExpressionKeys(property)) {
bindings[key] = 'options' bindings[key] = BindingTypes.OPTIONS
} }
} }
@ -1357,7 +1386,7 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
// methods: { foo() {} } // methods: { foo() {} }
// computed: { foo() {} } // computed: { foo() {} }
for (const key of getObjectExpressionKeys(property.value)) { for (const key of getObjectExpressionKeys(property.value)) {
bindings[key] = 'options' bindings[key] = BindingTypes.OPTIONS
} }
} }
} }
@ -1380,7 +1409,10 @@ function analyzeBindingsFromOptions(node: ObjectExpression): BindingMetadata {
bodyItem.argument.type === 'ObjectExpression' bodyItem.argument.type === 'ObjectExpression'
) { ) {
for (const key of getObjectExpressionKeys(bodyItem.argument)) { for (const key of getObjectExpressionKeys(bodyItem.argument)) {
bindings[key] = property.key.name bindings[key] =
property.key.name === 'setup'
? BindingTypes.SETUP
: BindingTypes.DATA
} }
} }
} }

View File

@ -13,7 +13,7 @@ import { ParserPlugin } from '@babel/parser'
export function genCssVarsCode( export function genCssVarsCode(
varsExp: string, varsExp: string,
scoped: boolean, scoped: boolean,
knownBindings?: Record<string, string | boolean> knownBindings?: Record<string, any>
) { ) {
const exp = createSimpleExpression(varsExp, false) const exp = createSimpleExpression(varsExp, false)
const context = createTransformContext(createRoot([]), { const context = createTransformContext(createRoot([]), {