feat(compiler-dom/runtime-dom): stringify eligible static trees
This commit is contained in:
parent
e861c6da90
commit
27913e661a
@ -48,7 +48,8 @@ import {
|
||||
WITH_SCOPE_ID,
|
||||
WITH_DIRECTIVES,
|
||||
CREATE_BLOCK,
|
||||
OPEN_BLOCK
|
||||
OPEN_BLOCK,
|
||||
CREATE_STATIC
|
||||
} from './runtimeHelpers'
|
||||
import { ImportItem } from './transform'
|
||||
|
||||
@ -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(', ')
|
||||
|
@ -5,7 +5,8 @@ export {
|
||||
CompilerOptions,
|
||||
ParserOptions,
|
||||
TransformOptions,
|
||||
CodegenOptions
|
||||
CodegenOptions,
|
||||
HoistTransform
|
||||
} from './options'
|
||||
export { baseParse, TextModes } from './parse'
|
||||
export {
|
||||
|
@ -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
|
||||
@ -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
|
||||
|
@ -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` : ``
|
||||
@ -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`,
|
||||
|
@ -115,6 +115,7 @@ export function createTransformContext(
|
||||
cacheHandlers = false,
|
||||
nodeTransforms = [],
|
||||
directiveTransforms = {},
|
||||
transformHoist = null,
|
||||
isBuiltInComponent = NOOP,
|
||||
scopeId = null,
|
||||
ssr = false,
|
||||
@ -128,6 +129,7 @@ export function createTransformContext(
|
||||
cacheHandlers,
|
||||
nodeTransforms,
|
||||
directiveTransforms,
|
||||
transformHoist,
|
||||
isBuiltInComponent,
|
||||
scopeId,
|
||||
ssr,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -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 {
|
||||
|
116
packages/compiler-dom/src/stringifyStatic.ts
Normal file
116
packages/compiler-dom/src/stringifyStatic.ts
Normal file
@ -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 ''
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
VNode,
|
||||
VNodeArrayChildren,
|
||||
createVNode,
|
||||
isSameVNodeType
|
||||
isSameVNodeType,
|
||||
Static
|
||||
} from './vnode'
|
||||
import {
|
||||
ComponentInternalInstance,
|
||||
@ -28,7 +29,8 @@ import {
|
||||
EMPTY_ARR,
|
||||
isReservedProp,
|
||||
isFunction,
|
||||
PatchFlags
|
||||
PatchFlags,
|
||||
NOOP
|
||||
} from '@vue/shared'
|
||||
import {
|
||||
queueJob,
|
||||
@ -88,8 +90,15 @@ export interface RendererOptions<HostNode = any, HostElement = any> {
|
||||
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> = (
|
||||
@ -197,7 +206,9 @@ export function createRenderer<
|
||||
parentNode: hostParentNode,
|
||||
nextSibling: hostNextSibling,
|
||||
querySelector: hostQuerySelector,
|
||||
setScopeId: hostSetScopeId
|
||||
setScopeId: hostSetScopeId = NOOP,
|
||||
cloneNode: hostCloneNode,
|
||||
insertStaticContent: hostInsertStaticContent
|
||||
} = options
|
||||
|
||||
const internals: RendererInternals<HostNode, HostElement> = {
|
||||
@ -233,6 +244,11 @@ export function createRenderer<
|
||||
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,
|
||||
@ -336,6 +352,26 @@ export function createRenderer<
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -374,9 +410,15 @@ export function createRenderer<
|
||||
isSVG: boolean,
|
||||
optimized: boolean
|
||||
) {
|
||||
const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG))
|
||||
let el: HostElement
|
||||
const { type, props, shapeFlag, transition, scopeId } = vnode
|
||||
|
||||
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) {
|
||||
@ -418,6 +460,8 @@ export function createRenderer<
|
||||
if (transition != null && !transition.persisted) {
|
||||
transition.beforeEnter(el)
|
||||
}
|
||||
}
|
||||
|
||||
hostInsert(el, container, anchor)
|
||||
const vnodeMountedHook = props && props.onVnodeMounted
|
||||
if (
|
||||
@ -776,8 +820,14 @@ export function createRenderer<
|
||||
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) {
|
||||
@ -825,7 +875,7 @@ export function createRenderer<
|
||||
// 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
|
||||
|
@ -39,6 +39,7 @@ export const Portal = (Symbol(__DEV__ ? 'Portal' : undefined) as any) as {
|
||||
}
|
||||
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
|
||||
@ -46,6 +47,7 @@ export type VNodeTypes =
|
||||
| typeof Fragment
|
||||
| typeof Portal
|
||||
| typeof Text
|
||||
| typeof Static
|
||||
| typeof Comment
|
||||
| typeof SuspenseImpl
|
||||
|
||||
@ -328,6 +330,10 @@ export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
|
||||
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
|
||||
|
@ -1,8 +1,13 @@
|
||||
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 {
|
||||
@ -10,37 +15,50 @@ export const nodeOps = {
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -4,23 +4,19 @@ import { patchAttr } from './modules/attrs'
|
||||
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':
|
||||
|
Loading…
Reference in New Issue
Block a user