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) {
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
        if (walk(child)) {
 | 
			
		||||
          return true
 | 
			
		||||
        if (child.props.length > 0) {
 | 
			
		||||
          ec++
 | 
			
		||||
          if (ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT) {
 | 
			
		||||
            return true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        walk(child)
 | 
			
		||||
        if (bailed) {
 | 
			
		||||
          return false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return walk(node)
 | 
			
		||||
  return walk(node) ? [nc, ec] : false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stringifyElement(
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user