]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(compiler): improve node stringification to support adjacent nodes
authorEvan You <yyx990803@gmail.com>
Fri, 15 May 2020 16:58:44 +0000 (12:58 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 15 May 2020 16:58:44 +0000 (12:58 -0400)
packages/compiler-core/src/ast.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts
packages/compiler-dom/src/transforms/stringifyStatic.ts

index 417f9cf37c43d6528ce2a83bd50c3cd78bb8ae6b..3e843b8a25d7f31259bbdd8f2a994069efc00c1e 100644 (file)
@@ -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.
index 087d8bcfe604750cf68ff02cca3ca4b742f32e1c..7b2c888674a9757d5a37fe68cb6d87be66905a03 100644 (file)
@@ -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 {
   /**
index 12c9023b6262d331bc8bc69db6ce2c3397fdd2b6..fc03af4c27c1118118009fbba042d730e3841dc2 100644 (file)
@@ -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)
index f15a9d0637164b61ffb5f16aa65638348cc1768e..59ba445ccbe6a6f42d3ac5e9e60cbdbd2fa267b2 100644 (file)
@@ -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(
index 3ed6cb2c045da5739ff255095055b092a4e69556..01cf7da2d1fb97490d15046c8b6e0d3d3fdd9038 100644 (file)
@@ -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'
 
+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 = (node, context) => {
-  if (shouldOptimize(node)) {
-    return createCallExpression(context.helper(CREATE_STATIC), [
-      JSON.stringify(stringifyElement(node, context))
-    ])
-  } else {
-    return node.codegenNode!
+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
   }
 }
 
-export const enum StringifyThresholds {
-  ELEMENT_WITH_BINDING_COUNT = 5,
-  NODE_COUNT = 20
-}
+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(