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
|
content: string
|
||||||
isStatic: boolean
|
isStatic: boolean
|
||||||
isConstant: 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
|
* an expression parsed as the params of a function will track
|
||||||
* the identifiers declared inside the function body.
|
* 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 { TextModes } from './parse'
|
||||||
import { CompilerError } from './errors'
|
import { CompilerError } from './errors'
|
||||||
import {
|
import {
|
||||||
@ -52,9 +52,9 @@ export interface ParserOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type HoistTransform = (
|
export type HoistTransform = (
|
||||||
node: PlainElementNode,
|
children: TemplateChildNode[],
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
) => JSChildNode
|
) => void
|
||||||
|
|
||||||
export interface TransformOptions {
|
export interface TransformOptions {
|
||||||
/**
|
/**
|
||||||
|
@ -230,12 +230,14 @@ export function createTransformContext(
|
|||||||
},
|
},
|
||||||
hoist(exp) {
|
hoist(exp) {
|
||||||
context.hoists.push(exp)
|
context.hoists.push(exp)
|
||||||
return createSimpleExpression(
|
const identifier = createSimpleExpression(
|
||||||
`_hoisted_${context.hoists.length}`,
|
`_hoisted_${context.hoists.length}`,
|
||||||
false,
|
false,
|
||||||
exp.loc,
|
exp.loc,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
identifier.hoisted = exp
|
||||||
|
return identifier
|
||||||
},
|
},
|
||||||
cache(exp, isVNode = false) {
|
cache(exp, isVNode = false) {
|
||||||
return createCacheExpression(++context.cached, exp, isVNode)
|
return createCacheExpression(++context.cached, exp, isVNode)
|
||||||
|
@ -54,10 +54,7 @@ function walk(
|
|||||||
// whole tree is static
|
// whole tree is static
|
||||||
;(child.codegenNode as VNodeCall).patchFlag =
|
;(child.codegenNode as VNodeCall).patchFlag =
|
||||||
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
|
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
|
||||||
const hoisted = context.transformHoist
|
child.codegenNode = context.hoist(child.codegenNode!)
|
||||||
? context.transformHoist(child, context)
|
|
||||||
: child.codegenNode!
|
|
||||||
child.codegenNode = context.hoist(hoisted)
|
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
// node may contain dynamic children, but its props may be eligible for
|
// 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(
|
export function isStaticNode(
|
||||||
|
@ -10,7 +10,11 @@ import {
|
|||||||
createCallExpression,
|
createCallExpression,
|
||||||
HoistTransform,
|
HoistTransform,
|
||||||
CREATE_STATIC,
|
CREATE_STATIC,
|
||||||
ExpressionNode
|
ExpressionNode,
|
||||||
|
ElementTypes,
|
||||||
|
PlainElementNode,
|
||||||
|
JSChildNode,
|
||||||
|
createSimpleExpression
|
||||||
} from '@vue/compiler-core'
|
} from '@vue/compiler-core'
|
||||||
import {
|
import {
|
||||||
isVoidTag,
|
isVoidTag,
|
||||||
@ -24,41 +28,113 @@ import {
|
|||||||
stringifyStyle
|
stringifyStyle
|
||||||
} from '@vue/shared'
|
} 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 {
|
export const enum StringifyThresholds {
|
||||||
ELEMENT_WITH_BINDING_COUNT = 5,
|
ELEMENT_WITH_BINDING_COUNT = 5,
|
||||||
NODE_COUNT = 20
|
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 dataAriaRE = /^(data|aria)-/
|
||||||
const isStringifiableAttr = (name: string) => {
|
const isStringifiableAttr = (name: string) => {
|
||||||
return isKnownAttr(name) || dataAriaRE.test(name)
|
return isKnownAttr(name) || dataAriaRE.test(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opt-in heuristics based on:
|
const replaceHoist = (
|
||||||
// 1. number of elements with attributes > 5.
|
node: PlainElementNode,
|
||||||
// 2. OR: number of total nodes > 20
|
replacement: JSChildNode,
|
||||||
// For some simple trees, the performance can actually be worse.
|
context: TransformContext
|
||||||
// it is only worth it when the tree is complex enough
|
) => {
|
||||||
// (e.g. big piece of static content)
|
const hoistToReplace = (node.codegenNode as SimpleExpressionNode).hoisted!
|
||||||
function shouldOptimize(node: ElementNode): boolean {
|
context.hoists[context.hoists.indexOf(hoistToReplace)] = replacement
|
||||||
let bindingThreshold = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
|
}
|
||||||
let nodeThreshold = StringifyThresholds.NODE_COUNT
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
let bailed = false
|
||||||
const bail = () => {
|
const bail = (): false => {
|
||||||
bailed = true
|
bailed = true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -67,7 +143,7 @@ function shouldOptimize(node: ElementNode): boolean {
|
|||||||
// output compared to imperative node insertions.
|
// output compared to imperative node insertions.
|
||||||
// probably only need to check for most common case
|
// probably only need to check for most common case
|
||||||
// i.e. non-phrasing-content tags inside `<p>`
|
// 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++) {
|
for (let i = 0; i < node.props.length; i++) {
|
||||||
const p = node.props[i]
|
const p = node.props[i]
|
||||||
// bail on non-attr bindings
|
// bail on non-attr bindings
|
||||||
@ -97,26 +173,28 @@ function shouldOptimize(node: ElementNode): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i = 0; i < node.children.length; i++) {
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
if (--nodeThreshold === 0) {
|
nc++
|
||||||
|
if (nc >= StringifyThresholds.NODE_COUNT) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const child = node.children[i]
|
const child = node.children[i]
|
||||||
if (child.type === NodeTypes.ELEMENT) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
if (walk(child)) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
walk(child)
|
||||||
if (bailed) {
|
if (bailed) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return walk(node)
|
return walk(node) ? [nc, ec] : false
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringifyElement(
|
function stringifyElement(
|
||||||
|
Loading…
Reference in New Issue
Block a user