wip(compiler): improve node stringification to support adjacent nodes
This commit is contained in:
parent
c2f3ee4dc0
commit
cb9444807e
@ -194,6 +194,11 @@ export interface SimpleExpressionNode extends Node {
|
||||
content: string
|
||||
isStatic: boolean
|
||||
isConstant: boolean
|
||||
/**
|
||||
* Indicates this is an identifier for a hoist vnode call and points to the
|
||||
* hoisted node.
|
||||
*/
|
||||
hoisted?: JSChildNode
|
||||
/**
|
||||
* an expression parsed as the params of a function will track
|
||||
* the identifiers declared inside the function body.
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
|
||||
import { ElementNode, Namespace, TemplateChildNode } from './ast'
|
||||
import { TextModes } from './parse'
|
||||
import { CompilerError } from './errors'
|
||||
import {
|
||||
@ -52,9 +52,9 @@ export interface ParserOptions {
|
||||
}
|
||||
|
||||
export type HoistTransform = (
|
||||
node: PlainElementNode,
|
||||
children: TemplateChildNode[],
|
||||
context: TransformContext
|
||||
) => JSChildNode
|
||||
) => void
|
||||
|
||||
export interface TransformOptions {
|
||||
/**
|
||||
|
@ -230,12 +230,14 @@ export function createTransformContext(
|
||||
},
|
||||
hoist(exp) {
|
||||
context.hoists.push(exp)
|
||||
return createSimpleExpression(
|
||||
const identifier = createSimpleExpression(
|
||||
`_hoisted_${context.hoists.length}`,
|
||||
false,
|
||||
exp.loc,
|
||||
true
|
||||
)
|
||||
identifier.hoisted = exp
|
||||
return identifier
|
||||
},
|
||||
cache(exp, isVNode = false) {
|
||||
return createCacheExpression(++context.cached, exp, isVNode)
|
||||
|
@ -54,10 +54,7 @@ function walk(
|
||||
// whole tree is static
|
||||
;(child.codegenNode as VNodeCall).patchFlag =
|
||||
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
|
||||
const hoisted = context.transformHoist
|
||||
? context.transformHoist(child, context)
|
||||
: child.codegenNode!
|
||||
child.codegenNode = context.hoist(hoisted)
|
||||
child.codegenNode = context.hoist(child.codegenNode!)
|
||||
continue
|
||||
} else {
|
||||
// node may contain dynamic children, but its props may be eligible for
|
||||
@ -100,6 +97,10 @@ function walk(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (context.transformHoist) {
|
||||
context.transformHoist(children, context)
|
||||
}
|
||||
}
|
||||
|
||||
export function isStaticNode(
|
||||
|
@ -10,7 +10,11 @@ import {
|
||||
createCallExpression,
|
||||
HoistTransform,
|
||||
CREATE_STATIC,
|
||||
ExpressionNode
|
||||
ExpressionNode,
|
||||
ElementTypes,
|
||||
PlainElementNode,
|
||||
JSChildNode,
|
||||
createSimpleExpression
|
||||
} from '@vue/compiler-core'
|
||||
import {
|
||||
isVoidTag,
|
||||
@ -24,41 +28,113 @@ import {
|
||||
stringifyStyle
|
||||
} from '@vue/shared'
|
||||
|
||||
// Turn eligible hoisted static trees into stringied static nodes, e.g.
|
||||
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
|
||||
// This is only performed in non-in-browser compilations.
|
||||
export const stringifyStatic: HoistTransform = (node, context) => {
|
||||
if (shouldOptimize(node)) {
|
||||
return createCallExpression(context.helper(CREATE_STATIC), [
|
||||
JSON.stringify(stringifyElement(node, context))
|
||||
])
|
||||
} else {
|
||||
return node.codegenNode!
|
||||
}
|
||||
}
|
||||
|
||||
export const enum StringifyThresholds {
|
||||
ELEMENT_WITH_BINDING_COUNT = 5,
|
||||
NODE_COUNT = 20
|
||||
}
|
||||
|
||||
// Turn eligible hoisted static trees into stringied static nodes, e.g.
|
||||
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
|
||||
// This is only performed in non-in-browser compilations.
|
||||
export const stringifyStatic: HoistTransform = (children, context) => {
|
||||
let nc = 0 // current node count
|
||||
let ec = 0 // current element with binding count
|
||||
const currentEligibleNodes: PlainElementNode[] = []
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
const hoisted = getHoistedNode(child)
|
||||
if (hoisted) {
|
||||
// presence of hoisted means child must be a plain element Node
|
||||
const node = child as PlainElementNode
|
||||
const result = analyzeNode(node)
|
||||
if (result) {
|
||||
// node is stringifiable, record state
|
||||
nc += result[0]
|
||||
ec += result[1]
|
||||
currentEligibleNodes.push(node)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// we only reach here if we ran into a node that is not stringifiable
|
||||
// check if currently analyzed nodes meet criteria for stringification.
|
||||
if (
|
||||
nc >= StringifyThresholds.NODE_COUNT ||
|
||||
ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
) {
|
||||
// combine all currently eligible nodes into a single static vnode call
|
||||
const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
|
||||
JSON.stringify(
|
||||
currentEligibleNodes
|
||||
.map(node => stringifyElement(node, context))
|
||||
.join('')
|
||||
),
|
||||
// the 2nd argument indicates the number of DOM nodes this static vnode
|
||||
// will insert / hydrate
|
||||
String(currentEligibleNodes.length)
|
||||
])
|
||||
// replace the first node's hoisted expression with the static vnode call
|
||||
replaceHoist(currentEligibleNodes[0], staticCall, context)
|
||||
|
||||
const n = currentEligibleNodes.length
|
||||
if (n > 1) {
|
||||
for (let j = 1; j < n; j++) {
|
||||
// for the merged nodes, set their hoisted expression to null
|
||||
replaceHoist(
|
||||
currentEligibleNodes[j],
|
||||
createSimpleExpression(`null`, false),
|
||||
context
|
||||
)
|
||||
}
|
||||
// also remove merged nodes from children
|
||||
const deleteCount = n - 1
|
||||
children.splice(i - n + 1, deleteCount)
|
||||
// adjust iteration index
|
||||
i -= deleteCount
|
||||
}
|
||||
}
|
||||
|
||||
// reset state
|
||||
nc = 0
|
||||
ec = 0
|
||||
currentEligibleNodes.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
const getHoistedNode = (node: TemplateChildNode) =>
|
||||
node.type === NodeTypes.ELEMENT &&
|
||||
node.tagType === ElementTypes.ELEMENT &&
|
||||
node.codegenNode &&
|
||||
node.codegenNode.type === NodeTypes.SIMPLE_EXPRESSION &&
|
||||
node.codegenNode.hoisted
|
||||
|
||||
const dataAriaRE = /^(data|aria)-/
|
||||
const isStringifiableAttr = (name: string) => {
|
||||
return isKnownAttr(name) || dataAriaRE.test(name)
|
||||
}
|
||||
|
||||
// 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 = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
||||
let nodeThreshold = StringifyThresholds.NODE_COUNT
|
||||
const replaceHoist = (
|
||||
node: PlainElementNode,
|
||||
replacement: JSChildNode,
|
||||
context: TransformContext
|
||||
) => {
|
||||
const hoistToReplace = (node.codegenNode as SimpleExpressionNode).hoisted!
|
||||
context.hoists[context.hoists.indexOf(hoistToReplace)] = replacement
|
||||
}
|
||||
|
||||
/**
|
||||
* for a hoisted node, analyze it and return:
|
||||
* - false: bailed (contains runtime constant)
|
||||
* - [x, y] where
|
||||
* - x is the number of nodes inside
|
||||
* - y is the number of element with bindings inside
|
||||
*/
|
||||
function analyzeNode(node: PlainElementNode): [number, number] | false {
|
||||
let nc = 1 // node count
|
||||
let ec = node.props.length > 0 ? 1 : 0 // element w/ binding count
|
||||
let bailed = false
|
||||
const bail = () => {
|
||||
const bail = (): false => {
|
||||
bailed = true
|
||||
return false
|
||||
}
|
||||
@ -67,7 +143,7 @@ function shouldOptimize(node: ElementNode): boolean {
|
||||
// output compared to imperative node insertions.
|
||||
// probably only need to check for most common case
|
||||
// i.e. non-phrasing-content tags inside `<p>`
|
||||
function walk(node: ElementNode) {
|
||||
function walk(node: ElementNode): boolean {
|
||||
for (let i = 0; i < node.props.length; i++) {
|
||||
const p = node.props[i]
|
||||
// bail on non-attr bindings
|
||||
@ -97,26 +173,28 @@ function shouldOptimize(node: ElementNode): boolean {
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
if (--nodeThreshold === 0) {
|
||||
nc++
|
||||
if (nc >= StringifyThresholds.NODE_COUNT) {
|
||||
return true
|
||||
}
|
||||
const child = node.children[i]
|
||||
if (child.type === NodeTypes.ELEMENT) {
|
||||
if (child.props.length > 0 && --bindingThreshold === 0) {
|
||||
if (child.props.length > 0) {
|
||||
ec++
|
||||
if (ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT) {
|
||||
return true
|
||||
}
|
||||
if (walk(child)) {
|
||||
return true
|
||||
}
|
||||
walk(child)
|
||||
if (bailed) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
return walk(node)
|
||||
return walk(node) ? [nc, ec] : false
|
||||
}
|
||||
|
||||
function stringifyElement(
|
||||
|
Loading…
Reference in New Issue
Block a user