]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(compiler-dom/runtime-dom): stringify eligible static trees
authorEvan You <yyx990803@gmail.com>
Wed, 12 Feb 2020 16:56:42 +0000 (11:56 -0500)
committerEvan You <yyx990803@gmail.com>
Wed, 12 Feb 2020 16:56:42 +0000 (11:56 -0500)
13 files changed:
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/index.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/runtimeHelpers.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts
packages/compiler-dom/src/index.ts
packages/compiler-dom/src/stringifyStatic.ts [new file with mode: 0644]
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
packages/runtime-dom/src/nodeOps.ts
packages/runtime-dom/src/patchProp.ts

index 8014416f22704357f4859fe4f6050a51f65a4123..9987c1321f7ef869907289a58bd5be100a445fe2 100644 (file)
@@ -48,7 +48,8 @@ import {
   WITH_SCOPE_ID,
   WITH_DIRECTIVES,
   CREATE_BLOCK,
-  OPEN_BLOCK
+  OPEN_BLOCK,
+  CREATE_STATIC
 } from './runtimeHelpers'
 import { ImportItem } from './transform'
 
@@ -309,7 +310,12 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
       // has check cost, but hoists are lifted out of the function - we need
       // to provide the helper here.
       if (ast.hoists.length) {
-        const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
+        const staticHelpers = [
+          CREATE_VNODE,
+          CREATE_COMMENT,
+          CREATE_TEXT,
+          CREATE_STATIC
+        ]
           .filter(helper => ast.helpers.includes(helper))
           .map(aliasHelper)
           .join(', ')
index 63cc6c15fdcee7af3b64ce5eb52dd17cd67b4e7a..6a224eb4ac433abb858ed71de84730119635b322 100644 (file)
@@ -5,7 +5,8 @@ export {
   CompilerOptions,
   ParserOptions,
   TransformOptions,
-  CodegenOptions
+  CodegenOptions,
+  HoistTransform
 } from './options'
 export { baseParse, TextModes } from './parse'
 export {
index b729ac3d07d5059d2d5a2433c294c7acdd0a6856..dc9d3631e16aa976d8898a2d4f0de7a2be4fcaac 100644 (file)
@@ -1,7 +1,11 @@
-import { ElementNode, Namespace } from './ast'
+import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
 import { TextModes } from './parse'
 import { CompilerError } from './errors'
-import { NodeTransform, DirectiveTransform } from './transform'
+import {
+  NodeTransform,
+  DirectiveTransform,
+  TransformContext
+} from './transform'
 
 export interface ParserOptions {
   isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
@@ -26,9 +30,17 @@ export interface ParserOptions {
   onError?: (error: CompilerError) => void
 }
 
+export type HoistTransform = (
+  node: PlainElementNode,
+  context: TransformContext
+) => JSChildNode
+
 export interface TransformOptions {
   nodeTransforms?: NodeTransform[]
   directiveTransforms?: Record<string, DirectiveTransform | undefined>
+  // an optional hook to transform a node being hoisted.
+  // used by compiler-dom to turn hoisted nodes into stringified HTML vnodes.
+  transformHoist?: HoistTransform | null
   isBuiltInComponent?: (tag: string) => symbol | void
   // Transform expressions like {{ foo }} to `_ctx.foo`.
   // If this option is false, the generated code will be wrapped in a
index b684780214c4f7d111b47165b72bec5de9aac9c4..11267c92989f8a4a682b3733f9e145ca06e7bc52 100644 (file)
@@ -8,6 +8,7 @@ export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
 export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
 export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``)
 export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``)
+export const CREATE_STATIC = Symbol(__DEV__ ? `createStaticVNode` : ``)
 export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
 export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
   __DEV__ ? `resolveDynamicComponent` : ``
@@ -40,6 +41,7 @@ export const helperNameMap: any = {
   [CREATE_VNODE]: `createVNode`,
   [CREATE_COMMENT]: `createCommentVNode`,
   [CREATE_TEXT]: `createTextVNode`,
+  [CREATE_STATIC]: `createStaticVNode`,
   [RESOLVE_COMPONENT]: `resolveComponent`,
   [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
   [RESOLVE_DIRECTIVE]: `resolveDirective`,
index 758dc8357b92fb07e180ddc533af876e44013c37..d69e9da452d7192e59ee25479e30bb99e08ae703 100644 (file)
@@ -115,6 +115,7 @@ export function createTransformContext(
     cacheHandlers = false,
     nodeTransforms = [],
     directiveTransforms = {},
+    transformHoist = null,
     isBuiltInComponent = NOOP,
     scopeId = null,
     ssr = false,
@@ -128,6 +129,7 @@ export function createTransformContext(
     cacheHandlers,
     nodeTransforms,
     directiveTransforms,
+    transformHoist,
     isBuiltInComponent,
     scopeId,
     ssr,
index 2dd514bee1f79319a263b0ad8e2e0415a3088b87..2bd0b608e4d282741c82490815b627e049657fcc 100644 (file)
@@ -52,7 +52,10 @@ function walk(
     ) {
       if (!doNotHoistNode && isStaticNode(child, resultCache)) {
         // whole tree is static
-        child.codegenNode = context.hoist(child.codegenNode!)
+        const hoisted = context.transformHoist
+          ? context.transformHoist(child, context)
+          : child.codegenNode!
+        child.codegenNode = context.hoist(hoisted)
         continue
       } else {
         // node may contain dynamic children, but its props may be eligible for
index 5ae5c1b5ee7fba85673387b204bf74fad4069c47..ca3051538187ca3382a272d1ece29efbb39b1185 100644 (file)
@@ -18,6 +18,7 @@ import { transformModel } from './transforms/vModel'
 import { transformOn } from './transforms/vOn'
 import { transformShow } from './transforms/vShow'
 import { warnTransitionChildren } from './transforms/warnTransitionChildren'
+import { stringifyStatic } from './stringifyStatic'
 
 export const parserOptions = __BROWSER__
   ? parserOptionsMinimal
@@ -41,17 +42,16 @@ export function compile(
   template: string,
   options: CompilerOptions = {}
 ): CodegenResult {
-  const result = baseCompile(template, {
+  return baseCompile(template, {
     ...parserOptions,
     ...options,
     nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])],
     directiveTransforms: {
       ...DOMDirectiveTransforms,
       ...(options.directiveTransforms || {})
-    }
+    },
+    transformHoist: __BROWSER__ ? null : stringifyStatic
   })
-  // debugger
-  return result
 }
 
 export function parse(template: string, options: ParserOptions = {}): RootNode {
diff --git a/packages/compiler-dom/src/stringifyStatic.ts b/packages/compiler-dom/src/stringifyStatic.ts
new file mode 100644 (file)
index 0000000..cb3fa3f
--- /dev/null
@@ -0,0 +1,116 @@
+import {
+  NodeTypes,
+  ElementNode,
+  TransformContext,
+  TemplateChildNode,
+  SimpleExpressionNode,
+  createCallExpression,
+  HoistTransform,
+  CREATE_STATIC
+} from '@vue/compiler-core'
+import { isVoidTag, isString, isSymbol, escapeHtml } from '@vue/shared'
+
+// Turn eligible hoisted static trees into stringied static nodes, e.g.
+//   const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
+export const stringifyStatic: HoistTransform = (node, context) => {
+  if (shouldOptimize(node)) {
+    return createCallExpression(context.helper(CREATE_STATIC), [
+      JSON.stringify(stringifyElement(node, context))
+    ])
+  } else {
+    return node.codegenNode!
+  }
+}
+
+// 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 = 5
+  let nodeThreshold = 20
+
+  function walk(node: ElementNode) {
+    for (let i = 0; i < node.children.length; i++) {
+      if (--nodeThreshold === 0) {
+        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
+        }
+      }
+    }
+    return false
+  }
+
+  return walk(node)
+}
+
+function stringifyElement(
+  node: ElementNode,
+  context: TransformContext
+): string {
+  let res = `<${node.tag}`
+  for (let i = 0; i < node.props.length; i++) {
+    const p = node.props[i]
+    if (p.type === NodeTypes.ATTRIBUTE) {
+      res += ` ${p.name}`
+      if (p.value) {
+        res += `="${p.value.content}"`
+      }
+    } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
+      // constant v-bind, e.g. :foo="1"
+      // TODO
+    }
+  }
+  if (context.scopeId) {
+    res += ` ${context.scopeId}`
+  }
+  res += `>`
+  for (let i = 0; i < node.children.length; i++) {
+    res += stringifyNode(node.children[i], context)
+  }
+  if (!isVoidTag(node.tag)) {
+    res += `</${node.tag}>`
+  }
+  return res
+}
+
+function stringifyNode(
+  node: string | TemplateChildNode,
+  context: TransformContext
+): string {
+  if (isString(node)) {
+    return node
+  }
+  if (isSymbol(node)) {
+    return ``
+  }
+  switch (node.type) {
+    case NodeTypes.ELEMENT:
+      return stringifyElement(node, context)
+    case NodeTypes.TEXT:
+      return escapeHtml(node.content)
+    case NodeTypes.COMMENT:
+      return `<!--${escapeHtml(node.content)}-->`
+    case NodeTypes.INTERPOLATION:
+      // constants
+      // TODO check eval
+      return (node.content as SimpleExpressionNode).content
+    case NodeTypes.COMPOUND_EXPRESSION:
+      // TODO proper handling
+      return node.children.map((c: any) => stringifyNode(c, context)).join('')
+    case NodeTypes.TEXT_CALL:
+      return stringifyNode(node.content, context)
+    default:
+      // static trees will not contain if/for nodes
+      return ''
+  }
+}
index 01ba0344ff5e12b59f6cfa48010d03228778d230..92919307863f36df1b82d1b4ff8279e43e2c39ff 100644 (file)
@@ -85,7 +85,12 @@ export { toHandlers } from './helpers/toHandlers'
 export { renderSlot } from './helpers/renderSlot'
 export { createSlots } from './helpers/createSlots'
 export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId'
-export { setBlockTracking, createTextVNode, createCommentVNode } from './vnode'
+export {
+  setBlockTracking,
+  createTextVNode,
+  createCommentVNode,
+  createStaticVNode
+} from './vnode'
 // Since @vue/shared is inlined into final builds,
 // when re-exporting from @vue/shared we need to avoid relying on their original
 // types so that the bundled d.ts does not attempt to import from it.
index e2f87c8f277d8b3241a9e7e1cbcba13271328ac4..888451f5c60b34b72a68caff49fd09ebfc1a54ac 100644 (file)
@@ -8,7 +8,8 @@ import {
   VNode,
   VNodeArrayChildren,
   createVNode,
-  isSameVNodeType
+  isSameVNodeType,
+  Static
 } from './vnode'
 import {
   ComponentInternalInstance,
@@ -28,7 +29,8 @@ import {
   EMPTY_ARR,
   isReservedProp,
   isFunction,
-  PatchFlags
+  PatchFlags,
+  NOOP
 } from '@vue/shared'
 import {
   queueJob,
@@ -88,8 +90,15 @@ export interface RendererOptions<HostNode = any, HostElement = any> {
   setElementText(node: HostElement, text: string): void
   parentNode(node: HostNode): HostElement | null
   nextSibling(node: HostNode): HostNode | null
-  querySelector(selector: string): HostElement | null
-  setScopeId(el: HostNode, id: string): void
+  querySelector?(selector: string): HostElement | null
+  setScopeId?(el: HostElement, id: string): void
+  cloneNode?(node: HostNode): HostNode
+  insertStaticContent?(
+    content: string,
+    parent: HostElement,
+    anchor: HostNode | null,
+    isSVG: boolean
+  ): HostElement
 }
 
 export type RootRenderFunction<HostNode, HostElement> = (
@@ -197,7 +206,9 @@ export function createRenderer<
     parentNode: hostParentNode,
     nextSibling: hostNextSibling,
     querySelector: hostQuerySelector,
-    setScopeId: hostSetScopeId
+    setScopeId: hostSetScopeId = NOOP,
+    cloneNode: hostCloneNode,
+    insertStaticContent: hostInsertStaticContent
   } = options
 
   const internals: RendererInternals<HostNode, HostElement> = {
@@ -233,6 +244,11 @@ export function createRenderer<
       case Comment:
         processCommentNode(n1, n2, container, anchor)
         break
+      case Static:
+        if (n1 == null) {
+          mountStaticNode(n2, container, anchor, isSVG)
+        } // static nodes are noop on patch
+        break
       case Fragment:
         processFragment(
           n1,
@@ -336,6 +352,26 @@ export function createRenderer<
     }
   }
 
+  function mountStaticNode(
+    n2: HostVNode,
+    container: HostElement,
+    anchor: HostNode | null,
+    isSVG: boolean
+  ) {
+    if (n2.el != null && hostCloneNode !== undefined) {
+      hostInsert(hostCloneNode(n2.el), container, anchor)
+    } else {
+      // static nodes are only present when used with compiler-dom/runtime-dom
+      // which guarantees presence of hostInsertStaticContent.
+      n2.el = hostInsertStaticContent!(
+        n2.children as string,
+        container,
+        anchor,
+        isSVG
+      )
+    }
+  }
+
   function processElement(
     n1: HostVNode | null,
     n2: HostVNode,
@@ -374,50 +410,58 @@ export function createRenderer<
     isSVG: boolean,
     optimized: boolean
   ) {
-    const el = (vnode.el = hostCreateElement(vnode.type as string, isSVG))
+    let el: HostElement
     const { type, props, shapeFlag, transition, scopeId } = vnode
-
-    // props
-    if (props != null) {
-      for (const key in props) {
-        if (isReservedProp(key)) continue
-        hostPatchProp(el, key, props[key], null, isSVG)
+    if (vnode.el != null && hostCloneNode !== undefined) {
+      // If a vnode has non-null el, it means it's being reused.
+      // Only static vnodes can be reused, so its mounted DOM nodes should be
+      // exactly the same, and we can simply do a clone here.
+      el = vnode.el = hostCloneNode(vnode.el) as HostElement
+    } else {
+      el = vnode.el = hostCreateElement(vnode.type as string, isSVG)
+      // props
+      if (props != null) {
+        for (const key in props) {
+          if (isReservedProp(key)) continue
+          hostPatchProp(el, key, props[key], null, isSVG)
+        }
+        if (props.onVnodeBeforeMount != null) {
+          invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
+        }
       }
-      if (props.onVnodeBeforeMount != null) {
-        invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
+
+      // scopeId
+      if (__BUNDLER__) {
+        if (scopeId !== null) {
+          hostSetScopeId(el, scopeId)
+        }
+        const treeOwnerId = parentComponent && parentComponent.type.__scopeId
+        // vnode's own scopeId and the current patched component's scopeId is
+        // different - this is a slot content node.
+        if (treeOwnerId != null && treeOwnerId !== scopeId) {
+          hostSetScopeId(el, treeOwnerId + '-s')
+        }
       }
-    }
 
-    // scopeId
-    if (__BUNDLER__) {
-      if (scopeId !== null) {
-        hostSetScopeId(el, scopeId)
+      // children
+      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
+        hostSetElementText(el, vnode.children as string)
+      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
+        mountChildren(
+          vnode.children as HostVNodeChildren,
+          el,
+          null,
+          parentComponent,
+          parentSuspense,
+          isSVG && type !== 'foreignObject',
+          optimized || vnode.dynamicChildren !== null
+        )
       }
-      const treeOwnerId = parentComponent && parentComponent.type.__scopeId
-      // vnode's own scopeId and the current patched component's scopeId is
-      // different - this is a slot content node.
-      if (treeOwnerId != null && treeOwnerId !== scopeId) {
-        hostSetScopeId(el, treeOwnerId + '-s')
+      if (transition != null && !transition.persisted) {
+        transition.beforeEnter(el)
       }
     }
 
-    // children
-    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
-      hostSetElementText(el, vnode.children as string)
-    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
-      mountChildren(
-        vnode.children as HostVNodeChildren,
-        el,
-        null,
-        parentComponent,
-        parentSuspense,
-        isSVG && type !== 'foreignObject',
-        optimized || vnode.dynamicChildren !== null
-      )
-    }
-    if (transition != null && !transition.persisted) {
-      transition.beforeEnter(el)
-    }
     hostInsert(el, container, anchor)
     const vnodeMountedHook = props && props.onVnodeMounted
     if (
@@ -776,8 +820,14 @@ export function createRenderer<
     const targetSelector = n2.props && n2.props.target
     const { patchFlag, shapeFlag, children } = n2
     if (n1 == null) {
+      if (__DEV__ && isString(targetSelector) && !hostQuerySelector) {
+        warn(
+          `Current renderer does not support string target for Portals. ` +
+            `(missing querySelector renderer option)`
+        )
+      }
       const target = (n2.target = isString(targetSelector)
-        ? hostQuerySelector(targetSelector)
+        ? hostQuerySelector!(targetSelector)
         : targetSelector)
       if (target != null) {
         if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
@@ -825,7 +875,7 @@ export function createRenderer<
       // target changed
       if (targetSelector !== (n1.props && n1.props.target)) {
         const nextTarget = (n2.target = isString(targetSelector)
-          ? hostQuerySelector(targetSelector)
+          ? hostQuerySelector!(targetSelector)
           : targetSelector)
         if (nextTarget != null) {
           // move content
index dd31a0ac86b4e90160954bf9d71cdf967adc7523..475323b910ad5df2714b14676d371c2487b828df 100644 (file)
@@ -39,6 +39,7 @@ export const Portal = (Symbol(__DEV__ ? 'Portal' : undefined) as any) as {
 }
 export const Text = Symbol(__DEV__ ? 'Text' : undefined)
 export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
+export const Static = Symbol(__DEV__ ? 'Static' : undefined)
 
 export type VNodeTypes =
   | string
@@ -46,6 +47,7 @@ export type VNodeTypes =
   | typeof Fragment
   | typeof Portal
   | typeof Text
+  | typeof Static
   | typeof Comment
   | typeof SuspenseImpl
 
@@ -328,6 +330,10 @@ export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
   return createVNode(Text, null, text, flag)
 }
 
+export function createStaticVNode(content: string): VNode {
+  return createVNode(Static, null, content)
+}
+
 export function createCommentVNode(
   text: string = '',
   // when used as the v-else branch, the comment node must be created as a
index 291eecd05a5b41488df9b947200ef10e78321c54..9b51161256625c8edd34d0be5523c4604056f88f 100644 (file)
@@ -1,8 +1,13 @@
+import { RendererOptions } from '@vue/runtime-core/src'
+
 const doc = (typeof document !== 'undefined' ? document : null) as Document
 const svgNS = 'http://www.w3.org/2000/svg'
 
-export const nodeOps = {
-  insert: (child: Node, parent: Node, anchor?: Node) => {
+let tempContainer: HTMLElement
+let tempSVGContainer: SVGElement
+
+export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
+  insert: (child, parent, anchor) => {
     if (anchor != null) {
       parent.insertBefore(child, anchor)
     } else {
@@ -10,37 +15,50 @@ export const nodeOps = {
     }
   },
 
-  remove: (child: Node) => {
+  remove: child => {
     const parent = child.parentNode
     if (parent != null) {
       parent.removeChild(child)
     }
   },
 
-  createElement: (tag: string, isSVG?: boolean): Element =>
+  createElement: (tag, isSVG): Element =>
     isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag),
 
-  createText: (text: string): Text => doc.createTextNode(text),
+  createText: text => doc.createTextNode(text),
 
-  createComment: (text: string): Comment => doc.createComment(text),
+  createComment: text => doc.createComment(text),
 
-  setText: (node: Text, text: string) => {
+  setText: (node, text) => {
     node.nodeValue = text
   },
 
-  setElementText: (el: HTMLElement, text: string) => {
+  setElementText: (el, text) => {
     el.textContent = text
   },
 
-  parentNode: (node: Node): HTMLElement | null =>
-    node.parentNode as HTMLElement,
+  parentNode: node => node.parentNode as Element | null,
 
-  nextSibling: (node: Node): Node | null => node.nextSibling,
+  nextSibling: node => node.nextSibling,
 
-  querySelector: (selector: string): Element | null =>
-    doc.querySelector(selector),
+  querySelector: selector => doc.querySelector(selector),
 
-  setScopeId(el: Element, id: string) {
+  setScopeId(el, id) {
     el.setAttribute(id, '')
+  },
+
+  cloneNode(el) {
+    return el.cloneNode(true)
+  },
+
+  insertStaticContent(content, parent, anchor, isSVG) {
+    const temp = isSVG
+      ? tempSVGContainer ||
+        (tempSVGContainer = doc.createElementNS(svgNS, 'svg'))
+      : tempContainer || (tempContainer = doc.createElement('div'))
+    temp.innerHTML = content
+    const node = temp.children[0]
+    nodeOps.insert(node, parent, anchor)
+    return node
   }
 }
index a99eb90b02a564e73d09df658fbb5321c2cd15dc..fa2ff3f46cea78a886af9c4059b473e16c6fd300 100644 (file)
@@ -4,23 +4,19 @@ import { patchAttr } from './modules/attrs'
 import { patchDOMProp } from './modules/props'
 import { patchEvent } from './modules/events'
 import { isOn } from '@vue/shared'
-import {
-  ComponentInternalInstance,
-  SuspenseBoundary,
-  VNode
-} from '@vue/runtime-core'
+import { RendererOptions } from '@vue/runtime-core'
 
-export function patchProp(
-  el: Element,
-  key: string,
-  nextValue: any,
-  prevValue: any,
-  isSVG: boolean,
-  prevChildren?: VNode[],
-  parentComponent?: ComponentInternalInstance,
-  parentSuspense?: SuspenseBoundary<Node, Element>,
-  unmountChildren?: any
-) {
+export const patchProp: RendererOptions<Node, Element>['patchProp'] = (
+  el,
+  key,
+  nextValue,
+  prevValue,
+  isSVG = false,
+  prevChildren,
+  parentComponent,
+  parentSuspense,
+  unmountChildren
+) => {
   switch (key) {
     // special
     case 'class':