wip(compiler): improve node stringification to support adjacent nodes

This commit is contained in:
Evan You 2020-05-15 12:58:44 -04:00
parent c2f3ee4dc0
commit cb9444807e
5 changed files with 127 additions and 41 deletions

View File

@ -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.

View File

@ -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 {
/** /**

View File

@ -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)

View File

@ -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(

View File

@ -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) {
return true ec++
} if (ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT) {
if (walk(child)) { return true
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(