Skip to content

Commit

Permalink
feat(compiler-dom/runtime-dom): stringify eligible static trees
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Feb 12, 2020
1 parent e861c6d commit 27913e6
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 84 deletions.
10 changes: 8 additions & 2 deletions packages/compiler-core/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ import {
WITH_SCOPE_ID,
WITH_DIRECTIVES,
CREATE_BLOCK,
OPEN_BLOCK
OPEN_BLOCK,
CREATE_STATIC
} from './runtimeHelpers'
import { ImportItem } from './transform'

Expand Down Expand Up @@ -309,7 +310,12 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
Expand Down
3 changes: 2 additions & 1 deletion packages/compiler-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ export {
CompilerOptions,
ParserOptions,
TransformOptions,
CodegenOptions
CodegenOptions,
HoistTransform
} from './options'
export { baseParse, TextModes } from './parse'
export {
Expand Down
16 changes: 14 additions & 2 deletions packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ElementNode, Namespace } from './ast'
import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
import { TextModes } from './parse'
import { CompilerError } from './errors'
import { NodeTransform, DirectiveTransform } from './transform'
import {
NodeTransform,
DirectiveTransform,
TransformContext
} from './transform'

export interface ParserOptions {
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
Expand All @@ -26,9 +30,17 @@ export interface ParserOptions {
onError?: (error: CompilerError) => void
}

export type HoistTransform = (
node: PlainElementNode,
context: TransformContext
) => JSChildNode

export interface TransformOptions {
nodeTransforms?: NodeTransform[]
directiveTransforms?: Record<string, DirectiveTransform | undefined>
// an optional hook to transform a node being hoisted.
// used by compiler-dom to turn hoisted nodes into stringified HTML vnodes.
transformHoist?: HoistTransform | null
isBuiltInComponent?: (tag: string) => symbol | void
// Transform expressions like {{ foo }} to `_ctx.foo`.
// If this option is false, the generated code will be wrapped in a
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-core/src/runtimeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``)
export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``)
export const CREATE_STATIC = Symbol(__DEV__ ? `createStaticVNode` : ``)
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
__DEV__ ? `resolveDynamicComponent` : ``
Expand Down Expand Up @@ -40,6 +41,7 @@ export const helperNameMap: any = {
[CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function createTransformContext(
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
transformHoist = null,
isBuiltInComponent = NOOP,
scopeId = null,
ssr = false,
Expand All @@ -128,6 +129,7 @@ export function createTransformContext(
cacheHandlers,
nodeTransforms,
directiveTransforms,
transformHoist,
isBuiltInComponent,
scopeId,
ssr,
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-core/src/transforms/hoistStatic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ function walk(
) {
if (!doNotHoistNode && isStaticNode(child, resultCache)) {
// whole tree is static
child.codegenNode = context.hoist(child.codegenNode!)
const hoisted = context.transformHoist
? context.transformHoist(child, context)
: child.codegenNode!
child.codegenNode = context.hoist(hoisted)
continue
} else {
// node may contain dynamic children, but its props may be eligible for
Expand Down
8 changes: 4 additions & 4 deletions packages/compiler-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { transformModel } from './transforms/vModel'
import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow'
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
import { stringifyStatic } from './stringifyStatic'

export const parserOptions = __BROWSER__
? parserOptionsMinimal
Expand All @@ -41,17 +42,16 @@ export function compile(
template: string,
options: CompilerOptions = {}
): CodegenResult {
const result = baseCompile(template, {
return baseCompile(template, {
...parserOptions,
...options,
nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])],
directiveTransforms: {
...DOMDirectiveTransforms,
...(options.directiveTransforms || {})
}
},
transformHoist: __BROWSER__ ? null : stringifyStatic
})
// debugger
return result
}

export function parse(template: string, options: ParserOptions = {}): RootNode {
Expand Down
116 changes: 116 additions & 0 deletions packages/compiler-dom/src/stringifyStatic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
NodeTypes,
ElementNode,
TransformContext,
TemplateChildNode,
SimpleExpressionNode,
createCallExpression,
HoistTransform,
CREATE_STATIC
} from '@vue/compiler-core'
import { isVoidTag, isString, isSymbol, escapeHtml } from '@vue/shared'

// Turn eligible hoisted static trees into stringied static nodes, e.g.
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
export const stringifyStatic: HoistTransform = (node, context) => {
if (shouldOptimize(node)) {
return createCallExpression(context.helper(CREATE_STATIC), [
JSON.stringify(stringifyElement(node, context))
])
} else {
return node.codegenNode!
}
}

// Opt-in heuristics based on:
// 1. number of elements with attributes > 5.
// 2. OR: number of total nodes > 20
// For some simple trees, the performance can actually be worse.
// it is only worth it when the tree is complex enough
// (e.g. big piece of static content)
function shouldOptimize(node: ElementNode): boolean {
let bindingThreshold = 5
let nodeThreshold = 20

function walk(node: ElementNode) {
for (let i = 0; i < node.children.length; i++) {
if (--nodeThreshold === 0) {
return true
}
const child = node.children[i]
if (child.type === NodeTypes.ELEMENT) {
if (child.props.length > 0 && --bindingThreshold === 0) {
return true
}
if (walk(child)) {
return true
}
}
}
return false
}

return walk(node)
}

function stringifyElement(
node: ElementNode,
context: TransformContext
): string {
let res = `<${node.tag}`
for (let i = 0; i < node.props.length; i++) {
const p = node.props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
res += ` ${p.name}`
if (p.value) {
res += `="${p.value.content}"`
}
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
// constant v-bind, e.g. :foo="1"
// TODO
}
}
if (context.scopeId) {
res += ` ${context.scopeId}`
}
res += `>`
for (let i = 0; i < node.children.length; i++) {
res += stringifyNode(node.children[i], context)
}
if (!isVoidTag(node.tag)) {
res += `</${node.tag}>`
}
return res
}

function stringifyNode(
node: string | TemplateChildNode,
context: TransformContext
): string {
if (isString(node)) {
return node
}
if (isSymbol(node)) {
return ``
}
switch (node.type) {
case NodeTypes.ELEMENT:
return stringifyElement(node, context)
case NodeTypes.TEXT:
return escapeHtml(node.content)
case NodeTypes.COMMENT:
return `<!--${escapeHtml(node.content)}-->`
case NodeTypes.INTERPOLATION:
// constants
// TODO check eval
return (node.content as SimpleExpressionNode).content
case NodeTypes.COMPOUND_EXPRESSION:
// TODO proper handling
return node.children.map((c: any) => stringifyNode(c, context)).join('')
case NodeTypes.TEXT_CALL:
return stringifyNode(node.content, context)
default:
// static trees will not contain if/for nodes
return ''
}
}
7 changes: 6 additions & 1 deletion packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot'
export { createSlots } from './helpers/createSlots'
export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId'
export { setBlockTracking, createTextVNode, createCommentVNode } from './vnode'
export {
setBlockTracking,
createTextVNode,
createCommentVNode,
createStaticVNode
} from './vnode'
// Since @vue/shared is inlined into final builds,
// when re-exporting from @vue/shared we need to avoid relying on their original
// types so that the bundled d.ts does not attempt to import from it.
Expand Down
Loading

0 comments on commit 27913e6

Please sign in to comment.