wip: template binding optimization

This commit is contained in:
Evan You 2020-07-10 22:12:25 -04:00
parent b51b79f5c4
commit b6cdd5621e
12 changed files with 81 additions and 30 deletions

View File

@ -64,7 +64,8 @@ export interface CodegenResult {
map?: RawSourceMap map?: RawSourceMap
} }
export interface CodegenContext extends Required<CodegenOptions> { export interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata'> {
source: string source: string
code: string code: string
line: number line: number
@ -204,16 +205,19 @@ export function generate(
} }
// enter render function // enter render function
const optimizeSources = options.bindingMetadata
? `, $props, $setup, $data, $options`
: ``
if (!ssr) { if (!ssr) {
if (genScopeId) { if (genScopeId) {
push(`const render = ${PURE_ANNOTATION}_withId(`) push(`const render = ${PURE_ANNOTATION}_withId(`)
} }
push(`function render(_ctx, _cache) {`) push(`function render(_ctx, _cache${optimizeSources}) {`)
} else { } else {
if (genScopeId) { if (genScopeId) {
push(`const ssrRender = ${PURE_ANNOTATION}_withId(`) push(`const ssrRender = ${PURE_ANNOTATION}_withId(`)
} }
push(`function ssrRender(_ctx, _push, _parent, _attrs) {`) push(`function ssrRender(_ctx, _push, _parent, _attrs${optimizeSources}) {`)
} }
indent() indent()

View File

@ -6,7 +6,8 @@ export {
ParserOptions, ParserOptions,
TransformOptions, TransformOptions,
CodegenOptions, CodegenOptions,
HoistTransform HoistTransform,
BindingMetadata
} from './options' } from './options'
export { baseParse, TextModes } from './parse' export { baseParse, TextModes } from './parse'
export { export {

View File

@ -57,6 +57,10 @@ export type HoistTransform = (
parent: ParentNode parent: ParentNode
) => void ) => void
export interface BindingMetadata {
[key: string]: 'data' | 'props' | 'setup' | 'options'
}
export interface TransformOptions { export interface TransformOptions {
/** /**
* An array of node transforms to be applied to every AST node. * An array of node transforms to be applied to every AST node.
@ -122,6 +126,11 @@ export interface TransformOptions {
* `ssrRender` option instead of `render`. * `ssrRender` option instead of `render`.
*/ */
ssr?: boolean ssr?: boolean
/**
* Optional binding metadata analyzed from script - used to optimize
* binding access when `prefixIdentifiers` is enabled.
*/
bindingMetadata?: BindingMetadata
onError?: (error: CompilerError) => void onError?: (error: CompilerError) => void
} }
@ -169,6 +178,7 @@ export interface CodegenOptions {
runtimeGlobalName?: string runtimeGlobalName?: string
// we need to know this during codegen to generate proper preambles // we need to know this during codegen to generate proper preambles
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
bindingMetadata?: BindingMetadata
// generate ssr-specific code? // generate ssr-specific code?
ssr?: boolean ssr?: boolean
} }

View File

@ -120,6 +120,7 @@ export function createTransformContext(
expressionPlugins = [], expressionPlugins = [],
scopeId = null, scopeId = null,
ssr = false, ssr = false,
bindingMetadata = {},
onError = defaultOnError onError = defaultOnError
}: TransformOptions }: TransformOptions
): TransformContext { ): TransformContext {
@ -135,6 +136,7 @@ export function createTransformContext(
expressionPlugins, expressionPlugins,
scopeId, scopeId,
ssr, ssr,
bindingMetadata,
onError, onError,
// state // state

View File

@ -1,8 +1,8 @@
// - Parse expressions in templates into compound expressions so that each // - Parse expressions in templates into compound expressions so that each
// identifier gets more accurate source-map locations. // identifier gets more accurate source-map locations.
// //
// - Prefix identifiers with `_ctx.` so that they are accessed from the render // - Prefix identifiers with `_ctx.` or `$xxx` (for known binding types) so that
// context // they are accessed from the right source
// //
// - This transform is only applied in non-browser builds because it relies on // - This transform is only applied in non-browser builds because it relies on
// an additional JavaScript parser. In the browser, there is no source-map // an additional JavaScript parser. In the browser, there is no source-map
@ -25,7 +25,8 @@ import {
import { import {
isGloballyWhitelisted, isGloballyWhitelisted,
makeMap, makeMap,
babelParserDefautPlugins babelParserDefautPlugins,
hasOwn
} from '@vue/shared' } from '@vue/shared'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { Node, Function, Identifier, ObjectProperty } from '@babel/types' import { Node, Function, Identifier, ObjectProperty } from '@babel/types'
@ -99,6 +100,14 @@ export function processExpression(
return node return node
} }
const { bindingMetadata } = context
const prefix = (raw: string) => {
const source = hasOwn(bindingMetadata, raw)
? `$` + bindingMetadata[raw]
: `_ctx`
return `${source}.${raw}`
}
// fast path if expression is a simple identifier. // fast path if expression is a simple identifier.
const rawExp = node.content const rawExp = node.content
// bail on parens to prevent any possible function invocations. // bail on parens to prevent any possible function invocations.
@ -110,7 +119,7 @@ export function processExpression(
!isGloballyWhitelisted(rawExp) && !isGloballyWhitelisted(rawExp) &&
!isLiteralWhitelisted(rawExp) !isLiteralWhitelisted(rawExp)
) { ) {
node.content = `_ctx.${rawExp}` node.content = prefix(rawExp)
} else if (!context.identifiers[rawExp] && !bailConstant) { } else if (!context.identifiers[rawExp] && !bailConstant) {
// mark node constant for hoisting unless it's referring a scope variable // mark node constant for hoisting unless it's referring a scope variable
node.isConstant = true node.isConstant = true
@ -148,7 +157,7 @@ export function processExpression(
const isDuplicate = (node: Node & PrefixMeta): boolean => const isDuplicate = (node: Node & PrefixMeta): boolean =>
ids.some(id => id.start === node.start) ids.some(id => id.start === node.start)
// walk the AST and look for identifiers that need to be prefixed with `_ctx.`. // walk the AST and look for identifiers that need to be prefixed.
walkJS(ast, { walkJS(ast, {
enter(node: Node & PrefixMeta, parent) { enter(node: Node & PrefixMeta, parent) {
if (node.type === 'Identifier') { if (node.type === 'Identifier') {
@ -160,7 +169,7 @@ export function processExpression(
// rewrite the value // rewrite the value
node.prefix = `${node.name}: ` node.prefix = `${node.name}: `
} }
node.name = `_ctx.${node.name}` node.name = prefix(node.name)
ids.push(node) ids.push(node)
} else if (!isStaticPropertyKey(node, parent)) { } else if (!isStaticPropertyKey(node, parent)) {
// The identifier is considered constant unless it's pointing to a // The identifier is considered constant unless it's pointing to a

View File

@ -1,4 +1,5 @@
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { BindingMetadata } from '@vue/compiler-core'
import { SFCDescriptor, SFCScriptBlock } from './parse' import { SFCDescriptor, SFCScriptBlock } from './parse'
import { parse, ParserPlugin } from '@babel/parser' import { parse, ParserPlugin } from '@babel/parser'
import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared' import { babelParserDefautPlugins, generateCodeFrame } from '@vue/shared'
@ -28,10 +29,6 @@ export interface SFCScriptCompileOptions {
babelParserPlugins?: ParserPlugin[] babelParserPlugins?: ParserPlugin[]
} }
export interface BindingMetadata {
[key: string]: 'data' | 'props' | 'setup' | 'ctx'
}
let hasWarned = false let hasWarned = false
/** /**

View File

@ -24,9 +24,10 @@ export {
SFCAsyncStyleCompileOptions, SFCAsyncStyleCompileOptions,
SFCStyleCompileResults SFCStyleCompileResults
} from './compileStyle' } from './compileStyle'
export { SFCScriptCompileOptions, BindingMetadata } from './compileScript' export { SFCScriptCompileOptions } from './compileScript'
export { export {
CompilerOptions, CompilerOptions,
CompilerError, CompilerError,
BindingMetadata,
generateCodeFrame generateCodeFrame
} from '@vue/compiler-core' } from '@vue/compiler-core'

View File

@ -3,17 +3,14 @@ import {
ElementNode, ElementNode,
SourceLocation, SourceLocation,
CompilerError, CompilerError,
TextModes TextModes,
BindingMetadata
} from '@vue/compiler-core' } from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom' import * as CompilerDOM from '@vue/compiler-dom'
import { RawSourceMap, SourceMapGenerator } from 'source-map' import { RawSourceMap, SourceMapGenerator } from 'source-map'
import { generateCodeFrame } from '@vue/shared' import { generateCodeFrame } from '@vue/shared'
import { TemplateCompiler } from './compileTemplate' import { TemplateCompiler } from './compileTemplate'
import { import { compileScript, SFCScriptCompileOptions } from './compileScript'
compileScript,
BindingMetadata,
SFCScriptCompileOptions
} from './compileScript'
export interface SFCParseOptions extends SFCScriptCompileOptions { export interface SFCParseOptions extends SFCScriptCompileOptions {
filename?: string filename?: string

View File

@ -77,10 +77,8 @@ export interface ComponentInternalOptions {
__file?: string __file?: string
} }
export interface FunctionalComponent< export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
P = {}, extends ComponentInternalOptions {
E extends EmitsOptions = {}
> extends ComponentInternalOptions {
// use of any here is intentional so it can be a valid JSX Element constructor // use of any here is intentional so it can be a valid JSX Element constructor
(props: P, ctx: SetupContext<E>): any (props: P, ctx: SetupContext<E>): any
props?: ComponentPropsOptions<P> props?: ComponentPropsOptions<P>
@ -142,7 +140,12 @@ export interface SetupContext<E = ObjectEmitsOptions> {
export type InternalRenderFunction = { export type InternalRenderFunction = {
( (
ctx: ComponentPublicInstance, ctx: ComponentPublicInstance,
cache: ComponentInternalInstance['renderCache'] cache: ComponentInternalInstance['renderCache'],
// for compiler-optimized bindings
$props: ComponentInternalInstance['props'],
$setup: ComponentInternalInstance['setupState'],
$data: ComponentInternalInstance['data'],
$options: ComponentInternalInstance['ctx']
): VNodeChild ): VNodeChild
_rc?: boolean // isRuntimeCompiled _rc?: boolean // isRuntimeCompiled
} }

View File

@ -119,7 +119,12 @@ export interface ComponentOptionsBase<
ctx: any, ctx: any,
push: (item: any) => void, push: (item: any) => void,
parentInstance: ComponentInternalInstance, parentInstance: ComponentInternalInstance,
attrs?: Data attrs: Data | undefined,
// for compiler-optimized bindings
$props: ComponentInternalInstance['props'],
$setup: ComponentInternalInstance['setupState'],
$data: ComponentInternalInstance['data'],
$options: ComponentInternalInstance['ctx']
) => void ) => void
/** /**

View File

@ -50,7 +50,11 @@ export function renderComponentRoot(
slots, slots,
attrs, attrs,
emit, emit,
renderCache render,
renderCache,
data,
setupState,
ctx
} = instance } = instance
let result let result
@ -65,7 +69,15 @@ export function renderComponentRoot(
// runtime-compiled render functions using `with` block. // runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy const proxyToUse = withProxy || proxy
result = normalizeVNode( result = normalizeVNode(
instance.render!.call(proxyToUse, proxyToUse!, renderCache) render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
) )
fallthroughAttrs = attrs fallthroughAttrs = attrs
} else { } else {

View File

@ -126,7 +126,17 @@ function renderComponentSubTree(
// set current rendering instance for asset resolution // set current rendering instance for asset resolution
setCurrentRenderingInstance(instance) setCurrentRenderingInstance(instance)
comp.ssrRender(instance.proxy, push, instance, attrs) comp.ssrRender(
instance.proxy,
push,
instance,
attrs,
// compiler-optimized bindings
instance.props,
instance.setupState,
instance.data,
instance.ctx
)
setCurrentRenderingInstance(null) setCurrentRenderingInstance(null)
} else if (instance.render) { } else if (instance.render) {
renderVNode(push, renderComponentRoot(instance), instance) renderVNode(push, renderComponentRoot(instance), instance)