]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(ssr): basic element hydration
authorEvan You <yyx990803@gmail.com>
Thu, 13 Feb 2020 22:47:00 +0000 (17:47 -0500)
committerEvan You <yyx990803@gmail.com>
Thu, 13 Feb 2020 22:47:00 +0000 (17:47 -0500)
packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap
packages/compiler-core/__tests__/scopeId.spec.ts
packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap
packages/compiler-core/src/transforms/hoistStatic.ts
packages/compiler-core/src/transforms/transformElement.ts
packages/compiler-dom/__tests__/transforms/__snapshots__/vShow.spec.ts.snap
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/vnode.ts
packages/runtime-dom/src/index.ts
packages/shared/src/patchFlags.ts

index 8a1497853f0aeb8c6ed52740edb1880805b53757..47e28dc221bf1967da15bd05bb8554f1b9725708 100644 (file)
@@ -5,8 +5,8 @@ exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = `
 const _withId = _withScopeId(\\"test\\")
 
 _pushScopeId(\\"test\\")
-const _hoisted_1 = _createVNode(\\"div\\", null, \\"hello\\", -1)
-const _hoisted_2 = _createVNode(\\"div\\", null, \\"world\\", -1)
+const _hoisted_1 = _createVNode(\\"div\\", null, \\"hello\\", -2 /* HOISTED */)
+const _hoisted_2 = _createVNode(\\"div\\", null, \\"world\\", -2 /* HOISTED */)
 _popScopeId()
 
 export const render = _withId(function render(_ctx, _cache) {
index 000b93739c16eea9af7789d8cad5bab702c84261..9b098a121032c88425234b6e4a8d3e133d1ee9ac 100644 (file)
@@ -4,6 +4,8 @@ import {
   PUSH_SCOPE_ID,
   POP_SCOPE_ID
 } from '../src/runtimeHelpers'
+import { PatchFlags } from '@vue/shared'
+import { genFlagText } from './testUtils'
 
 describe('scopeId compiler support', () => {
   test('should only work in module mode', () => {
@@ -81,8 +83,12 @@ describe('scopeId compiler support', () => {
     expect(code).toMatch(
       [
         `_pushScopeId("test")`,
-        `const _hoisted_1 = _createVNode("div", null, "hello", -1)`,
-        `const _hoisted_2 = _createVNode("div", null, "world", -1)`,
+        `const _hoisted_1 = _createVNode("div", null, "hello", ${genFlagText(
+          PatchFlags.HOISTED
+        )})`,
+        `const _hoisted_2 = _createVNode("div", null, "world", ${genFlagText(
+          PatchFlags.HOISTED
+        )})`,
         `_popScopeId()`
       ].join('\n')
     )
index 1667d09ee33dccb8f9f215876c5dcfd2a202e6d8..5912db0a92b064fe5b8b220e6d867f50b487e1e4 100644 (file)
@@ -4,7 +4,7 @@ exports[`compiler: hoistStatic transform hoist element with static key 1`] = `
 "const _Vue = Vue
 const { createVNode: _createVNode } = _Vue
 
-const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }, null, -1)
+const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }, null, -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -24,7 +24,7 @@ const { createVNode: _createVNode } = _Vue
 const _hoisted_1 = _createVNode(\\"p\\", null, [
   _createVNode(\\"span\\"),
   _createVNode(\\"span\\")
-], -1)
+], -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -43,7 +43,7 @@ const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _
 
 const _hoisted_1 = _createVNode(\\"div\\", null, [
   _createCommentVNode(\\"comment\\")
-], -1)
+], -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -60,8 +60,8 @@ exports[`compiler: hoistStatic transform hoist siblings with common non-hoistabl
 "const _Vue = Vue
 const { createVNode: _createVNode } = _Vue
 
-const _hoisted_1 = _createVNode(\\"span\\", null, null, -1)
-const _hoisted_2 = _createVNode(\\"div\\", null, null, -1)
+const _hoisted_1 = _createVNode(\\"span\\", null, null, -2 /* HOISTED */)
+const _hoisted_2 = _createVNode(\\"div\\", null, null, -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -79,7 +79,7 @@ exports[`compiler: hoistStatic transform hoist simple element 1`] = `
 "const _Vue = Vue
 const { createVNode: _createVNode } = _Vue
 
-const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -1)
+const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -105,7 +105,7 @@ return function render(_ctx, _cache) {
     const _directive_foo = _resolveDirective(\\"foo\\")
 
     return (_openBlock(), _createBlock(\\"div\\", null, [
-      _withDirectives(_createVNode(\\"div\\", _hoisted_1, null, 32 /* NEED_PATCH */), [
+      _withDirectives(_createVNode(\\"div\\", _hoisted_1, null, -1 /* NEED_PATCH */), [
         [_directive_foo]
       ])
     ]))
@@ -172,7 +172,7 @@ exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static t
 "const _Vue = Vue
 const { createVNode: _createVNode } = _Vue
 
-const _hoisted_1 = _createVNode(\\"span\\", null, \\"foo \\" + _toDisplayString(1) + \\" \\" + _toDisplayString(true), -1)
+const _hoisted_1 = _createVNode(\\"span\\", null, \\"foo \\" + _toDisplayString(1) + \\" \\" + _toDisplayString(true), -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -189,7 +189,7 @@ exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static t
 "const _Vue = Vue
 const { createVNode: _createVNode } = _Vue
 
-const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toDisplayString(1), -1)
+const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toDisplayString(1), -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -323,7 +323,7 @@ return function render(_ctx, _cache) {
     const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
 
     return (_openBlock(), _createBlock(\\"div\\", null, [
-      _createVNode(\\"div\\", { ref: foo }, null, 32 /* NEED_PATCH */)
+      _createVNode(\\"div\\", { ref: foo }, null, -1 /* NEED_PATCH */)
     ]))
   }
 }"
@@ -346,7 +346,7 @@ exports[`compiler: hoistStatic transform should hoist v-for children if static 1
 const { createVNode: _createVNode } = _Vue
 
 const _hoisted_1 = { id: \\"foo\\" }
-const _hoisted_2 = _createVNode(\\"span\\", null, null, -1)
+const _hoisted_2 = _createVNode(\\"span\\", null, null, -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
@@ -371,7 +371,7 @@ const _hoisted_1 = {
   key: 0,
   id: \\"foo\\"
 }
-const _hoisted_2 = _createVNode(\\"span\\", null, null, -1)
+const _hoisted_2 = _createVNode(\\"span\\", null, null, -2 /* HOISTED */)
 
 return function render(_ctx, _cache) {
   with (this) {
index 9b721ef8588aa26ead678fe0d5032244f11d5ff1..0d549e386f52404170fb44aa8ff46657b34b21a3 100644 (file)
@@ -142,7 +142,7 @@ return function render(_ctx, _cache) {
     const _directive_foo = _resolveDirective(\\"foo\\")
 
     return (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => {
-      return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */)), [
+      return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, -1 /* NEED_PATCH */)), [
         [_directive_foo]
       ])
     }), 256 /* UNKEYED_FRAGMENT */))
index 06d9eaf74bd9824452df661d133d0b6070150dd5..da44a1ecb56f10de686ef21b470498a827a7bfbf 100644 (file)
@@ -52,7 +52,8 @@ function walk(
     ) {
       if (!doNotHoistNode && isStaticNode(child, resultCache)) {
         // whole tree is static
-        ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + ``
+        ;(child.codegenNode as VNodeCall).patchFlag =
+          PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
         const hoisted = context.transformHoist
           ? context.transformHoist(child, context)
           : child.codegenNode!
index 2d8eead62d76d1497ca8f3c37141177561d2a871..2e48125455b71436a3285150f5d744923edf346a 100644 (file)
@@ -20,7 +20,7 @@ import {
   DirectiveArguments,
   createVNodeCall
 } from '../ast'
-import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared'
+import { PatchFlags, PatchFlagNames, isSymbol, isOn } from '@vue/shared'
 import { createCompilerError, ErrorCodes } from '../errors'
 import {
   RESOLVE_DIRECTIVE,
@@ -159,12 +159,18 @@ export const transformElement: NodeTransform = (node, context) => {
     // patchFlag & dynamicPropNames
     if (patchFlag !== 0) {
       if (__DEV__) {
-        const flagNames = Object.keys(PatchFlagNames)
-          .map(Number)
-          .filter(n => n > 0 && patchFlag & n)
-          .map(n => PatchFlagNames[n])
-          .join(`, `)
-        vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
+        if (patchFlag < 0) {
+          // special flags (negative and mutually exclusive)
+          vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */`
+        } else {
+          // bitwise flags
+          const flagNames = Object.keys(PatchFlagNames)
+            .map(Number)
+            .filter(n => n > 0 && patchFlag & n)
+            .map(n => PatchFlagNames[n])
+            .join(`, `)
+          vnodePatchFlag = patchFlag + ` /* ${flagNames} */`
+        }
       } else {
         vnodePatchFlag = String(patchFlag)
       }
@@ -256,20 +262,27 @@ export function buildProps(
   let hasRef = false
   let hasClassBinding = false
   let hasStyleBinding = false
+  let hasHydrationEventBinding = false
   let hasDynamicKeys = false
   const dynamicPropNames: string[] = []
 
   const analyzePatchFlag = ({ key, value }: Property) => {
     if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) {
+      const name = key.content
+      if (!isComponent && isOn(name) && name.toLowerCase() !== 'onclick') {
+        // This flag is for hydrating event handlers only. We omit the flag for
+        // click handlers becaues hydration gives click dedicated fast path.
+        hasHydrationEventBinding = true
+      }
       if (
         value.type === NodeTypes.JS_CACHE_EXPRESSION ||
         ((value.type === NodeTypes.SIMPLE_EXPRESSION ||
           value.type === NodeTypes.COMPOUND_EXPRESSION) &&
           isStaticNode(value))
       ) {
+        // skip if the prop is a cached handler or has constant value
         return
       }
-      const name = key.content
       if (name === 'ref') {
         hasRef = true
       } else if (name === 'class') {
@@ -430,9 +443,12 @@ export function buildProps(
     if (dynamicPropNames.length) {
       patchFlag |= PatchFlags.PROPS
     }
+    if (hasHydrationEventBinding) {
+      patchFlag |= PatchFlags.HYDRATE_EVENTS
+    }
   }
   if (patchFlag === 0 && (hasRef || runtimeDirectives.length > 0)) {
-    patchFlag |= PatchFlags.NEED_PATCH
+    patchFlag = PatchFlags.NEED_PATCH
   }
 
   return {
index c392948c8ab25f4c4f8c6a9cc832b52606cdb058..4bf5457165041703dd126378c6f55b92aa7eb55a 100644 (file)
@@ -7,7 +7,7 @@ return function render(_ctx, _cache) {
   with (this) {
     const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue
 
-    return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */)), [
+    return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, -1 /* NEED_PATCH */)), [
       [_vShow, a]
     ])
   }
index 01b66c4895c993c86fa4c6a23344fc7cd3d04b1b..3c0828df4a7d040f4a11119f31ba80acbdd53c10 100644 (file)
@@ -30,7 +30,8 @@ import {
   isReservedProp,
   isFunction,
   PatchFlags,
-  NOOP
+  NOOP,
+  isOn
 } from '@vue/shared'
 import {
   queueJob,
@@ -188,6 +189,7 @@ export function createRenderer<
   options: RendererOptions<HostNode, HostElement>
 ): {
   render: RootRenderFunction<HostNode, HostElement>
+  hydrate: RootRenderFunction<HostNode, HostElement>
   createApp: CreateAppFunction<HostElement>
 } {
   type HostVNode = VNode<HostNode, HostElement>
@@ -426,8 +428,9 @@ export function createRenderer<
       // props
       if (props != null) {
         for (const key in props) {
-          if (isReservedProp(key)) continue
-          hostPatchProp(el, key, props[key], null, isSVG)
+          if (!isReservedProp(key)) {
+            hostPatchProp(el, key, props[key], null, isSVG)
+          }
         }
         if (props.onVnodeBeforeMount != null) {
           invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
@@ -1813,8 +1816,118 @@ export function createRenderer<
     container._vnode = vnode
   }
 
+  function hydrate(vnode: HostVNode, container: any) {
+    hydrateNode(container.firstChild, vnode, container)
+    flushPostFlushCbs()
+  }
+
+  // TODO handle mismatches
+  function hydrateNode(
+    node: any,
+    vnode: HostVNode,
+    container: any,
+    parentComponent: ComponentInternalInstance | null = null
+  ): any {
+    const { type, shapeFlag } = vnode
+    switch (type) {
+      case Text:
+      case Comment:
+      case Static:
+        vnode.el = node
+        return node.nextSibling
+      case Fragment:
+        vnode.el = node
+        const anchor = (vnode.anchor = hydrateChildren(
+          node.nextSibling,
+          vnode.children as HostVNode[],
+          container,
+          parentComponent
+        ))
+        return anchor.nextSibling
+      case Portal:
+        // TODO
+        break
+      default:
+        if (shapeFlag & ShapeFlags.ELEMENT) {
+          return hydrateElement(node, vnode, parentComponent)
+        } else if (shapeFlag & ShapeFlags.COMPONENT) {
+          // TODO
+        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
+          // TODO
+        } else if (__DEV__) {
+          warn('Invalid HostVNode type:', type, `(${typeof type})`)
+        }
+    }
+  }
+
+  function hydrateElement(
+    el: any,
+    vnode: HostVNode,
+    parentComponent: ComponentInternalInstance | null
+  ) {
+    vnode.el = el
+    const { props, patchFlag } = vnode
+    // skip props & children if this is hoisted static nodes
+    if (patchFlag !== PatchFlags.HOISTED) {
+      // props
+      if (props !== null) {
+        if (
+          patchFlag & PatchFlags.FULL_PROPS ||
+          patchFlag & PatchFlags.HYDRATE_EVENTS
+        ) {
+          for (const key in props) {
+            if (!isReservedProp(key) && isOn(key)) {
+              hostPatchProp(el, key, props[key], null)
+            }
+          }
+        } else if (props.onClick != null) {
+          // Fast path for click listeners (which is most often) to avoid
+          // iterating through props.
+          hostPatchProp(el, 'onClick', props.onClick, null)
+        }
+        // vnode mounted hook
+        const { onVnodeMounted } = props
+        if (onVnodeMounted != null) {
+          queuePostFlushCb(() => {
+            invokeDirectiveHook(onVnodeMounted, parentComponent, vnode, null)
+          })
+        }
+      }
+      // children
+      if (
+        vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN &&
+        // skip if element has innerHTML / textContent
+        !(props !== null && (props.innerHTML || props.textContent))
+      ) {
+        hydrateChildren(
+          el.firstChild,
+          vnode.children as HostVNode[],
+          el,
+          parentComponent
+        )
+      }
+    }
+    return el.nextSibling
+  }
+
+  function hydrateChildren(
+    node: any,
+    vnodes: HostVNode[],
+    container: any,
+    parentComponent: ComponentInternalInstance | null = null
+  ) {
+    for (let i = 0; i < vnodes.length; i++) {
+      // TODO can skip normalizeVNode in optimized mode
+      // (need hint on rendered markup?)
+      const vnode = (vnodes[i] = normalizeVNode(vnodes[i]))
+      node = hydrateNode(node, vnode, container, parentComponent)
+    }
+    return node
+  }
+
   return {
     render,
+    hydrate,
     createApp: createAppAPI(render)
   }
 }
index 475323b910ad5df2714b14676d371c2487b828df..61d953a02abb54786624b5a34b285234364e1d47 100644 (file)
@@ -6,7 +6,8 @@ import {
   EMPTY_ARR,
   extend,
   normalizeClass,
-  normalizeStyle
+  normalizeStyle,
+  PatchFlags
 } from '@vue/shared'
 import {
   ComponentInternalInstance,
@@ -277,7 +278,11 @@ export function createVNode(
   if (
     shouldTrack > 0 &&
     currentBlock !== null &&
+    // the EVENTS flag is only for hydration and if it is the only flag, the
+    // vnode should not be considered dynamic.
+    patchFlag !== PatchFlags.HYDRATE_EVENTS &&
     (patchFlag > 0 ||
+      patchFlag === PatchFlags.NEED_PATCH ||
       shapeFlag & ShapeFlags.SUSPENSE ||
       shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
       shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
index 9c9c1b59ff67aade340b14568ee16e506199d473..8f4ec06f7925fd6866363eff8a00ad9aa45a3e01 100644 (file)
@@ -9,13 +9,18 @@ import { patchProp } from './patchProp'
 // Importing from the compiler, will be tree-shaken in prod
 import { isFunction, isString, isHTMLTag, isSVGTag } from '@vue/shared'
 
-const { render: baseRender, createApp: baseCreateApp } = createRenderer({
+const {
+  render: baseRender,
+  hydrate: baseHydrate,
+  createApp: baseCreateApp
+} = createRenderer({
   patchProp,
   ...nodeOps
 })
 
 // use explicit type casts here to avoid import() calls in rolled-up d.ts
 export const render = baseRender as RootRenderFunction<Node, Element>
+export const hydrate = baseHydrate as RootRenderFunction<Node, Element>
 
 export const createApp: CreateAppFunction<Element> = (...args) => {
   const app = baseCreateApp(...args)
index 86dff111931a394395cbc2944e4425127992ce42..d0f841b778dbcd17c9aea7ca262a76fcdea076ef 100644 (file)
@@ -40,12 +40,9 @@ export const enum PatchFlags {
   // exclusive with CLASS, STYLE and PROPS.
   FULL_PROPS = 1 << 4,
 
-  // Indicates an element that only needs non-props patching, e.g. ref or
-  // directives (onVnodeXXX hooks). It simply marks the vnode as "need patch",
-  // since every patched vnode checks for refs and onVnodeXXX hooks.
-  // This flag is never directly matched against, it simply serves as a non-zero
-  // value.
-  NEED_PATCH = 1 << 5,
+  // Indicates an element with event listeners (which need to be attached
+  // during hydration)
+  HYDRATE_EVENTS = 1 << 5,
 
   // Indicates a fragment whose children order doesn't change.
   STABLE_FRAGMENT = 1 << 6,
@@ -61,14 +58,28 @@ export const enum PatchFlags {
   // Components with this flag are always force updated.
   DYNAMIC_SLOTS = 1 << 9,
 
-  // A special flag that indicates a hoisted, static vnode.
-  HOISTED = -1,
+  // SPECIAL FLAGS -------------------------------------------------------------
+
+  // Special flags are negative integers. They are never matched against using
+  // bitwise operators (bitwise matching should only happen in branches where
+  // patchFlag > 0), and are mutually exclusive. When checking for a speical
+  // flag, simply check patchFlag === FLAG.
+
+  // Indicates an element that only needs non-props patching, e.g. ref or
+  // directives (onVnodeXXX hooks). since every patched vnode checks for refs
+  // and onVnodeXXX hooks, itt simply marks the vnode so that a parent block
+  // will track it.
+  NEED_PATCH = -1,
+
+  // Indicates a hoisted static vnode. This is a hint for hydration to skip
+  // the entire sub tree since static content never needs to be updated.
+  HOISTED = -2,
 
   // A special flag that indicates that the diffing algorithm should bail out
   // of optimized mode. This is only on block fragments created by renderSlot()
   // when encountering non-compiler generated slots (i.e. manually written
   // render functions, which should always be fully diffed)
-  BAIL = -2
+  BAIL = -3
 }
 
 // runtime object for public consumption
@@ -91,11 +102,13 @@ export const PatchFlagNames = {
   [PatchFlags.CLASS]: `CLASS`,
   [PatchFlags.STYLE]: `STYLE`,
   [PatchFlags.PROPS]: `PROPS`,
-  [PatchFlags.NEED_PATCH]: `NEED_PATCH`,
   [PatchFlags.FULL_PROPS]: `FULL_PROPS`,
+  [PatchFlags.HYDRATE_EVENTS]: `HYDRATE_EVENTS`,
   [PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`,
   [PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`,
   [PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`,
   [PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
+  [PatchFlags.NEED_PATCH]: `NEED_PATCH`,
+  [PatchFlags.HOISTED]: `HOISTED`,
   [PatchFlags.BAIL]: `BAIL`
 }