]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(ssr): should set ref on hydration
authorEvan You <yyx990803@gmail.com>
Thu, 21 May 2020 21:37:23 +0000 (17:37 -0400)
committerEvan You <yyx990803@gmail.com>
Thu, 21 May 2020 21:37:23 +0000 (17:37 -0400)
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/renderer.ts

index 947d1f2963feba2354486ac51c3affd4433f6f0d..49daa3023f3da1eb2e9b9f46335b405354f01a02 100644 (file)
@@ -124,6 +124,15 @@ describe('SSR hydration', () => {
     expect(vnode.el.innerHTML).toBe(`<span>bar</span><span class="bar"></span>`)
   })
 
+  test('element with ref', () => {
+    const el = ref()
+    const { vnode, container } = mountWithHydration('<div></div>', () =>
+      h('div', { ref: el })
+    )
+    expect(vnode.el).toBe(container.firstChild)
+    expect(el.value).toBe(vnode.el)
+  })
+
   test('Fragment', async () => {
     const msg = ref('foo')
     const fn = jest.fn()
index 5d8fd41fb951f9948b63c26eefbf5754472fe33b..b0d455b0af4337b5283550ffa7a0d2967992ae22 100644 (file)
@@ -12,7 +12,7 @@ import { ComponentOptions, ComponentInternalInstance } from './component'
 import { invokeDirectiveHook } from './directives'
 import { warn } from './warning'
 import { PatchFlags, ShapeFlags, isReservedProp, isOn } from '@vue/shared'
-import { RendererInternals, invokeVNodeHook } from './renderer'
+import { RendererInternals, invokeVNodeHook, setRef } from './renderer'
 import {
   SuspenseImpl,
   SuspenseBoundary,
@@ -88,74 +88,85 @@ export function createHydrationFunctions(
         isFragmentStart
       )
 
-    const { type, shapeFlag } = vnode
+    const { type, ref, shapeFlag } = vnode
     const domType = node.nodeType
     vnode.el = node
 
+    let nextNode: Node | null = null
     switch (type) {
       case Text:
         if (domType !== DOMNodeTypes.TEXT) {
-          return onMismatch()
-        }
-        if ((node as Text).data !== vnode.children) {
-          hasMismatch = true
-          __DEV__ &&
-            warn(
-              `Hydration text mismatch:` +
-                `\n- Client: ${JSON.stringify((node as Text).data)}` +
-                `\n- Server: ${JSON.stringify(vnode.children)}`
-            )
-          ;(node as Text).data = vnode.children as string
+          nextNode = onMismatch()
+        } else {
+          if ((node as Text).data !== vnode.children) {
+            hasMismatch = true
+            __DEV__ &&
+              warn(
+                `Hydration text mismatch:` +
+                  `\n- Client: ${JSON.stringify((node as Text).data)}` +
+                  `\n- Server: ${JSON.stringify(vnode.children)}`
+              )
+            ;(node as Text).data = vnode.children as string
+          }
+          nextNode = nextSibling(node)
         }
-        return nextSibling(node)
+        break
       case Comment:
         if (domType !== DOMNodeTypes.COMMENT || isFragmentStart) {
-          return onMismatch()
+          nextNode = onMismatch()
+        } else {
+          nextNode = nextSibling(node)
         }
-        return nextSibling(node)
+        break
       case Static:
         if (domType !== DOMNodeTypes.ELEMENT) {
-          return onMismatch()
-        }
-        // determine anchor, adopt content
-        let cur = node
-        // if the static vnode has its content stripped during build,
-        // adopt it from the server-rendered HTML.
-        const needToAdoptContent = !(vnode.children as string).length
-        for (let i = 0; i < vnode.staticCount; i++) {
-          if (needToAdoptContent) vnode.children += (cur as Element).outerHTML
-          if (i === vnode.staticCount - 1) {
-            vnode.anchor = cur
+          nextNode = onMismatch()
+        } else {
+          // determine anchor, adopt content
+          nextNode = node
+          // if the static vnode has its content stripped during build,
+          // adopt it from the server-rendered HTML.
+          const needToAdoptContent = !(vnode.children as string).length
+          for (let i = 0; i < vnode.staticCount; i++) {
+            if (needToAdoptContent)
+              vnode.children += (nextNode as Element).outerHTML
+            if (i === vnode.staticCount - 1) {
+              vnode.anchor = nextNode
+            }
+            nextNode = nextSibling(nextNode)!
           }
-          cur = nextSibling(cur)!
+          return nextNode
         }
-        return cur
+        break
       case Fragment:
         if (!isFragmentStart) {
-          return onMismatch()
+          nextNode = onMismatch()
+        } else {
+          nextNode = hydrateFragment(
+            node as Comment,
+            vnode,
+            parentComponent,
+            parentSuspense,
+            optimized
+          )
         }
-        return hydrateFragment(
-          node as Comment,
-          vnode,
-          parentComponent,
-          parentSuspense,
-          optimized
-        )
+        break
       default:
         if (shapeFlag & ShapeFlags.ELEMENT) {
           if (
             domType !== DOMNodeTypes.ELEMENT ||
             vnode.type !== (node as Element).tagName.toLowerCase()
           ) {
-            return onMismatch()
+            nextNode = onMismatch()
+          } else {
+            nextNode = hydrateElement(
+              node as Element,
+              vnode,
+              parentComponent,
+              parentSuspense,
+              optimized
+            )
           }
-          return hydrateElement(
-            node as Element,
-            vnode,
-            parentComponent,
-            parentSuspense,
-            optimized
-          )
         } else if (shapeFlag & ShapeFlags.COMPONENT) {
           // when setting up the render effect, if the initial vnode already
           // has .el set, the component will perform hydration instead of mount
@@ -182,24 +193,25 @@ export function createHydrationFunctions(
           // component may be async, so in the case of fragments we cannot rely
           // on component's rendered output to determine the end of the fragment
           // instead, we do a lookahead to find the end anchor node.
-          return isFragmentStart
+          nextNode = isFragmentStart
             ? locateClosingAsyncAnchor(node)
             : nextSibling(node)
         } else if (shapeFlag & ShapeFlags.TELEPORT) {
           if (domType !== DOMNodeTypes.COMMENT) {
-            return onMismatch()
+            nextNode = onMismatch()
+          } else {
+            nextNode = (vnode.type as typeof TeleportImpl).hydrate(
+              node,
+              vnode,
+              parentComponent,
+              parentSuspense,
+              optimized,
+              rendererInternals,
+              hydrateChildren
+            )
           }
-          return (vnode.type as typeof TeleportImpl).hydrate(
-            node,
-            vnode,
-            parentComponent,
-            parentSuspense,
-            optimized,
-            rendererInternals,
-            hydrateChildren
-          )
         } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
-          return (vnode.type as typeof SuspenseImpl).hydrate(
+          nextNode = (vnode.type as typeof SuspenseImpl).hydrate(
             node,
             vnode,
             parentComponent,
@@ -212,8 +224,13 @@ export function createHydrationFunctions(
         } else if (__DEV__) {
           warn('Invalid HostVNode type:', type, `(${typeof type})`)
         }
-        return null
     }
+
+    if (ref != null && parentComponent) {
+      setRef(ref, null, parentComponent, vnode)
+    }
+
+    return nextNode
   }
 
   const hydrateElement = (
@@ -386,7 +403,7 @@ export function createHydrationFunctions(
     parentComponent: ComponentInternalInstance | null,
     parentSuspense: SuspenseBoundary | null,
     isFragment: boolean
-  ) => {
+  ): Node | null => {
     hasMismatch = true
     __DEV__ &&
       warn(
index cf4b21db510daaee5e97225b4161230556bd4d02..f816459be87834f34e1657a1069732faa4ecb311 100644 (file)
@@ -47,7 +47,6 @@ import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
 import { updateProps } from './componentProps'
 import { updateSlots } from './componentSlots'
 import { pushWarningContext, popWarningContext, warn } from './warning'
-import { ComponentPublicInstance } from './componentProxy'
 import { createAppAPI, CreateAppFunction } from './apiCreateApp'
 import {
   SuspenseBoundary,
@@ -271,6 +270,55 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
   ? queueEffectWithSuspense
   : queuePostFlushCb
 
+export const setRef = (
+  rawRef: VNodeNormalizedRef,
+  oldRawRef: VNodeNormalizedRef | null,
+  parent: ComponentInternalInstance,
+  vnode: VNode | null
+) => {
+  const value = vnode
+    ? vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
+      ? vnode.component!.proxy
+      : vnode.el
+    : null
+  const [owner, ref] = rawRef
+  if (__DEV__ && !owner) {
+    warn(
+      `Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
+        `A vnode with ref must be created inside the render function.`
+    )
+    return
+  }
+  const oldRef = oldRawRef && oldRawRef[1]
+  const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
+  const setupState = owner.setupState
+
+  // unset old ref
+  if (oldRef != null && oldRef !== ref) {
+    if (isString(oldRef)) {
+      refs[oldRef] = null
+      if (hasOwn(setupState, oldRef)) {
+        setupState[oldRef] = null
+      }
+    } else if (isRef(oldRef)) {
+      oldRef.value = null
+    }
+  }
+
+  if (isString(ref)) {
+    refs[ref] = value
+    if (hasOwn(setupState, ref)) {
+      setupState[ref] = value
+    }
+  } else if (isRef(ref)) {
+    ref.value = value
+  } else if (isFunction(ref)) {
+    callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value, refs])
+  } else if (__DEV__) {
+    warn('Invalid template ref type:', value, `(${typeof value})`)
+  }
+}
+
 /**
  * The createRenderer function accepts two generic arguments:
  * HostNode and HostElement, corresponding to Node and Element types in the
@@ -440,9 +488,7 @@ function baseCreateRenderer(
 
     // set ref
     if (ref != null && parentComponent) {
-      const refValue =
-        shapeFlag & ShapeFlags.STATEFUL_COMPONENT ? n2.component!.proxy : n2.el
-      setRef(ref, n1 && n1.ref, parentComponent, refValue)
+      setRef(ref, n1 && n1.ref, parentComponent, n2)
     }
   }
 
@@ -1984,50 +2030,6 @@ function baseCreateRenderer(
     return hostNextSibling((vnode.anchor || vnode.el)!)
   }
 
-  const setRef = (
-    rawRef: VNodeNormalizedRef,
-    oldRawRef: VNodeNormalizedRef | null,
-    parent: ComponentInternalInstance,
-    value: RendererNode | ComponentPublicInstance | null
-  ) => {
-    const [owner, ref] = rawRef
-    if (__DEV__ && !owner) {
-      warn(
-        `Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
-          `A vnode with ref must be created inside the render function.`
-      )
-      return
-    }
-    const oldRef = oldRawRef && oldRawRef[1]
-    const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
-    const setupState = owner.setupState
-
-    // unset old ref
-    if (oldRef != null && oldRef !== ref) {
-      if (isString(oldRef)) {
-        refs[oldRef] = null
-        if (hasOwn(setupState, oldRef)) {
-          setupState[oldRef] = null
-        }
-      } else if (isRef(oldRef)) {
-        oldRef.value = null
-      }
-    }
-
-    if (isString(ref)) {
-      refs[ref] = value
-      if (hasOwn(setupState, ref)) {
-        setupState[ref] = value
-      }
-    } else if (isRef(ref)) {
-      ref.value = value
-    } else if (isFunction(ref)) {
-      callWithErrorHandling(ref, parent, ErrorCodes.FUNCTION_REF, [value, refs])
-    } else if (__DEV__) {
-      warn('Invalid template ref type:', value, `(${typeof value})`)
-    }
-  }
-
   /**
    * #1156
    * When a component is HMR-enabled, we need to make sure that all static nodes