WITH_SCOPE_ID,
WITH_DIRECTIVES,
CREATE_BLOCK,
- OPEN_BLOCK
+ OPEN_BLOCK,
+ CREATE_STATIC
} from './runtimeHelpers'
import { ImportItem } from './transform'
// 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(', ')
CompilerOptions,
ParserOptions,
TransformOptions,
- CodegenOptions
+ CodegenOptions,
+ HoistTransform
} from './options'
export { baseParse, TextModes } from './parse'
export {
-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
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
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` : ``
[CREATE_VNODE]: `createVNode`,
[CREATE_COMMENT]: `createCommentVNode`,
[CREATE_TEXT]: `createTextVNode`,
+ [CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
cacheHandlers = false,
nodeTransforms = [],
directiveTransforms = {},
+ transformHoist = null,
isBuiltInComponent = NOOP,
scopeId = null,
ssr = false,
cacheHandlers,
nodeTransforms,
directiveTransforms,
+ transformHoist,
isBuiltInComponent,
scopeId,
ssr,
) {
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
import { transformOn } from './transforms/vOn'
import { transformShow } from './transforms/vShow'
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
+import { stringifyStatic } from './stringifyStatic'
export const parserOptions = __BROWSER__
? parserOptionsMinimal
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 {
--- /dev/null
+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 ''
+ }
+}
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.
VNode,
VNodeArrayChildren,
createVNode,
- isSameVNodeType
+ isSameVNodeType,
+ Static
} from './vnode'
import {
ComponentInternalInstance,
EMPTY_ARR,
isReservedProp,
isFunction,
- PatchFlags
+ PatchFlags,
+ NOOP
} from '@vue/shared'
import {
queueJob,
setElementText(node: HostElement, text: string): void
parentNode(node: HostNode): HostElement | null
nextSibling(node: HostNode): HostNode | null
- querySelector(selector: string): HostElement | null
- setScopeId(el: HostNode, id: string): void
+ querySelector?(selector: string): HostElement | null
+ setScopeId?(el: HostElement, id: string): void
+ cloneNode?(node: HostNode): HostNode
+ insertStaticContent?(
+ content: string,
+ parent: HostElement,
+ anchor: HostNode | null,
+ isSVG: boolean
+ ): HostElement
}
export type RootRenderFunction<HostNode, HostElement> = (
parentNode: hostParentNode,
nextSibling: hostNextSibling,
querySelector: hostQuerySelector,
- setScopeId: hostSetScopeId
+ setScopeId: hostSetScopeId = NOOP,
+ cloneNode: hostCloneNode,
+ insertStaticContent: hostInsertStaticContent
} = options
const internals: RendererInternals<HostNode, HostElement> = {
case Comment:
processCommentNode(n1, n2, container, anchor)
break
+ case Static:
+ if (n1 == null) {
+ mountStaticNode(n2, container, anchor, isSVG)
+ } // static nodes are noop on patch
+ break
case Fragment:
processFragment(
n1,
}
}
+ function mountStaticNode(
+ n2: HostVNode,
+ container: HostElement,
+ anchor: HostNode | null,
+ isSVG: boolean
+ ) {
+ if (n2.el != null && hostCloneNode !== undefined) {
+ hostInsert(hostCloneNode(n2.el), container, anchor)
+ } else {
+ // static nodes are only present when used with compiler-dom/runtime-dom
+ // which guarantees presence of hostInsertStaticContent.
+ n2.el = hostInsertStaticContent!(
+ n2.children as string,
+ container,
+ anchor,
+ isSVG
+ )
+ }
+ }
+
function processElement(
n1: HostVNode | null,
n2: HostVNode,
isSVG: boolean,
optimized: boolean
) {
- const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG))
+ let el: HostElement
const { type, props, shapeFlag, transition, scopeId } = vnode
-
- // props
- if (props != null) {
- for (const key in props) {
- if (isReservedProp(key)) continue
- hostPatchProp(el, key, props[key], null, isSVG)
+ if (vnode.el != null && hostCloneNode !== undefined) {
+ // If a vnode has non-null el, it means it's being reused.
+ // Only static vnodes can be reused, so its mounted DOM nodes should be
+ // exactly the same, and we can simply do a clone here.
+ el = vnode.el = hostCloneNode(vnode.el) as HostElement
+ } else {
+ el = vnode.el = hostCreateElement(vnode.type as string, isSVG)
+ // props
+ if (props != null) {
+ for (const key in props) {
+ if (isReservedProp(key)) continue
+ hostPatchProp(el, key, props[key], null, isSVG)
+ }
+ if (props.onVnodeBeforeMount != null) {
+ invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
+ }
}
- if (props.onVnodeBeforeMount != null) {
- invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
+
+ // scopeId
+ if (__BUNDLER__) {
+ if (scopeId !== null) {
+ hostSetScopeId(el, scopeId)
+ }
+ const treeOwnerId = parentComponent && parentComponent.type.__scopeId
+ // vnode's own scopeId and the current patched component's scopeId is
+ // different - this is a slot content node.
+ if (treeOwnerId != null && treeOwnerId !== scopeId) {
+ hostSetScopeId(el, treeOwnerId + '-s')
+ }
}
- }
- // scopeId
- if (__BUNDLER__) {
- if (scopeId !== null) {
- hostSetScopeId(el, scopeId)
+ // children
+ if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
+ hostSetElementText(el, vnode.children as string)
+ } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
+ mountChildren(
+ vnode.children as HostVNodeChildren,
+ el,
+ null,
+ parentComponent,
+ parentSuspense,
+ isSVG && type !== 'foreignObject',
+ optimized || vnode.dynamicChildren !== null
+ )
}
- const treeOwnerId = parentComponent && parentComponent.type.__scopeId
- // vnode's own scopeId and the current patched component's scopeId is
- // different - this is a slot content node.
- if (treeOwnerId != null && treeOwnerId !== scopeId) {
- hostSetScopeId(el, treeOwnerId + '-s')
+ if (transition != null && !transition.persisted) {
+ transition.beforeEnter(el)
}
}
- // children
- if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
- hostSetElementText(el, vnode.children as string)
- } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
- mountChildren(
- vnode.children as HostVNodeChildren,
- el,
- null,
- parentComponent,
- parentSuspense,
- isSVG && type !== 'foreignObject',
- optimized || vnode.dynamicChildren !== null
- )
- }
- if (transition != null && !transition.persisted) {
- transition.beforeEnter(el)
- }
hostInsert(el, container, anchor)
const vnodeMountedHook = props && props.onVnodeMounted
if (
const targetSelector = n2.props && n2.props.target
const { patchFlag, shapeFlag, children } = n2
if (n1 == null) {
+ if (__DEV__ && isString(targetSelector) && !hostQuerySelector) {
+ warn(
+ `Current renderer does not support string target for Portals. ` +
+ `(missing querySelector renderer option)`
+ )
+ }
const target = (n2.target = isString(targetSelector)
- ? hostQuerySelector(targetSelector)
+ ? hostQuerySelector!(targetSelector)
: targetSelector)
if (target != null) {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// target changed
if (targetSelector !== (n1.props && n1.props.target)) {
const nextTarget = (n2.target = isString(targetSelector)
- ? hostQuerySelector(targetSelector)
+ ? hostQuerySelector!(targetSelector)
: targetSelector)
if (nextTarget != null) {
// move content
}
export const Text = Symbol(__DEV__ ? 'Text' : undefined)
export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
+export const Static = Symbol(__DEV__ ? 'Static' : undefined)
export type VNodeTypes =
| string
| typeof Fragment
| typeof Portal
| typeof Text
+ | typeof Static
| typeof Comment
| typeof SuspenseImpl
return createVNode(Text, null, text, flag)
}
+export function createStaticVNode(content: string): VNode {
+ return createVNode(Static, null, content)
+}
+
export function createCommentVNode(
text: string = '',
// when used as the v-else branch, the comment node must be created as a
+import { RendererOptions } from '@vue/runtime-core/src'
+
const doc = (typeof document !== 'undefined' ? document : null) as Document
const svgNS = 'http://www.w3.org/2000/svg'
-export const nodeOps = {
- insert: (child: Node, parent: Node, anchor?: Node) => {
+let tempContainer: HTMLElement
+let tempSVGContainer: SVGElement
+
+export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
+ insert: (child, parent, anchor) => {
if (anchor != null) {
parent.insertBefore(child, anchor)
} else {
}
},
- remove: (child: Node) => {
+ remove: child => {
const parent = child.parentNode
if (parent != null) {
parent.removeChild(child)
}
},
- createElement: (tag: string, isSVG?: boolean): Element =>
+ createElement: (tag, isSVG): Element =>
isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag),
- createText: (text: string): Text => doc.createTextNode(text),
+ createText: text => doc.createTextNode(text),
- createComment: (text: string): Comment => doc.createComment(text),
+ createComment: text => doc.createComment(text),
- setText: (node: Text, text: string) => {
+ setText: (node, text) => {
node.nodeValue = text
},
- setElementText: (el: HTMLElement, text: string) => {
+ setElementText: (el, text) => {
el.textContent = text
},
- parentNode: (node: Node): HTMLElement | null =>
- node.parentNode as HTMLElement,
+ parentNode: node => node.parentNode as Element | null,
- nextSibling: (node: Node): Node | null => node.nextSibling,
+ nextSibling: node => node.nextSibling,
- querySelector: (selector: string): Element | null =>
- doc.querySelector(selector),
+ querySelector: selector => doc.querySelector(selector),
- setScopeId(el: Element, id: string) {
+ setScopeId(el, id) {
el.setAttribute(id, '')
+ },
+
+ cloneNode(el) {
+ return el.cloneNode(true)
+ },
+
+ insertStaticContent(content, parent, anchor, isSVG) {
+ const temp = isSVG
+ ? tempSVGContainer ||
+ (tempSVGContainer = doc.createElementNS(svgNS, 'svg'))
+ : tempContainer || (tempContainer = doc.createElement('div'))
+ temp.innerHTML = content
+ const node = temp.children[0]
+ nodeOps.insert(node, parent, anchor)
+ return node
}
}
import { patchDOMProp } from './modules/props'
import { patchEvent } from './modules/events'
import { isOn } from '@vue/shared'
-import {
- ComponentInternalInstance,
- SuspenseBoundary,
- VNode
-} from '@vue/runtime-core'
+import { RendererOptions } from '@vue/runtime-core'
-export function patchProp(
- el: Element,
- key: string,
- nextValue: any,
- prevValue: any,
- isSVG: boolean,
- prevChildren?: VNode[],
- parentComponent?: ComponentInternalInstance,
- parentSuspense?: SuspenseBoundary<Node, Element>,
- unmountChildren?: any
-) {
+export const patchProp: RendererOptions<Node, Element>['patchProp'] = (
+ el,
+ key,
+ nextValue,
+ prevValue,
+ isSVG = false,
+ prevChildren,
+ parentComponent,
+ parentSuspense,
+ unmountChildren
+) => {
switch (key) {
// special
case 'class':