Просмотр исходного кода

fix(hydration): skip dynamic children in __child

daiwei 11 месяцев назад
Родитель
Сommit
3f3480c05b

+ 1 - 1
packages/compiler-vapor/__tests__/__snapshots__/compile.spec.ts.snap

@@ -157,7 +157,7 @@ export function render(_ctx, $props, $emit, $attrs, $slots) {
   const _component_Comp = _resolveComponent("Comp")
   const _component_Comp = _resolveComponent("Comp")
   const n0 = t0()
   const n0 = t0()
   const n3 = t1()
   const n3 = t1()
-  const n2 = _child(n3)
+  const n2 = _child(n3, 1)
   _setInsertionState(n3, 0)
   _setInsertionState(n3, 0)
   const n1 = _createComponentWithFallback(_component_Comp)
   const n1 = _createComponentWithFallback(_component_Comp)
   _renderEffect(() => {
   _renderEffect(() => {

+ 6 - 2
packages/compiler-vapor/src/generators/template.ts

@@ -82,11 +82,15 @@ export function genChildren(
         pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
         pushBlock(...genCall(helper('nthChild'), from, String(elementIndex)))
       }
       }
     } else {
     } else {
+      // offset is used to determine the child during hydration.
+      // if offset is not 0, we need to specify the offset to skip the dynamic
+      // children and get the correct child.
+      let childOffset = offset === 0 ? undefined : `${Math.abs(offset)}`
       if (elementIndex === 0) {
       if (elementIndex === 0) {
-        pushBlock(...genCall(helper('child'), from))
+        pushBlock(...genCall(helper('child'), from, childOffset))
       } else {
       } else {
         // check if there's a node that we can reuse from
         // check if there's a node that we can reuse from
-        let init = genCall(helper('child'), from)
+        let init = genCall(helper('child'), from, childOffset)
         if (elementIndex === 1) {
         if (elementIndex === 1) {
           init = genCall(helper('next'), init)
           init = genCall(helper('next'), init)
         } else if (elementIndex > 1) {
         } else if (elementIndex > 1) {

+ 37 - 0
packages/runtime-vapor/__tests__/hydration.spec.ts

@@ -2176,6 +2176,43 @@ describe('Vapor Mode hydration', () => {
       )
       )
     })
     })
 
 
+    test('mixed consecutive slot and element', async () => {
+      const data = reactive({
+        text: 'foo',
+        msg: 'hi',
+      })
+      const { container } = await testHydration(
+        `<template>
+          <components.Child>
+            <template #foo><span>{{data.text}}</span></template>
+            <template #bar><span>bar</span></template>
+          </components.Child>
+        </template>`,
+        {
+          Child: `<template><div><slot name="foo"/><slot name="bar"/><div>{{data.msg}}</div></div></template>`,
+        },
+        data,
+      )
+
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<div>hi</div>` +
+          `</div>`,
+      )
+
+      data.msg = 'bar'
+      await nextTick()
+      expect(container.innerHTML).toBe(
+        `<div>` +
+          `<!--[--><span>foo</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<!--[--><span>bar</span><!--]--><!--${slotAnchorLabel}-->` +
+          `<div>bar</div>` +
+          `</div>`,
+      )
+    })
+
     test('mixed slot and element', async () => {
     test('mixed slot and element', async () => {
       const data = reactive({
       const data = reactive({
         text: 'foo',
         text: 'foo',

+ 9 - 9
packages/runtime-vapor/src/dom/node.ts

@@ -41,7 +41,7 @@ export function _child(node: ParentNode): Node {
  *
  *
  *   Client Compiled Code (Simplified):
  *   Client Compiled Code (Simplified):
  *     const n2 = t0() // n2 = `<div> </div>`
  *     const n2 = t0() // n2 = `<div> </div>`
- *     const n1 = _child(n2) // n1 = text node
+ *     const n1 = _child(n2, 1) // n1 = text node
  *     // ... slot creation ...
  *     // ... slot creation ...
  *     _renderEffect(() => _setText(n1, _ctx.msg))
  *     _renderEffect(() => _setText(n1, _ctx.msg))
  *
  *
@@ -49,18 +49,18 @@ export function _child(node: ParentNode): Node {
  *
  *
  *   Hydration Mismatch:
  *   Hydration Mismatch:
  *   - During hydration, `n2` refers to the SSR `<div>`.
  *   - During hydration, `n2` refers to the SSR `<div>`.
- *   - `_child(n2)` would return `<!--[-->`.
+ *   - `_child(n2, 1)` would return `<!--[-->`.
  *   - The client code expects `n1` to be the text node, but gets the comment.
  *   - The client code expects `n1` to be the text node, but gets the comment.
  *     The subsequent `_setText(n1, ...)` would fail or target the wrong node.
  *     The subsequent `_setText(n1, ...)` would fail or target the wrong node.
  *
  *
  *   Solution (`__child`):
  *   Solution (`__child`):
- *   - `__child(n2)` is used during hydration. It skips the SSR fragment anchors
- *     (`<!--[-->...<!--]-->`) and any other non-content nodes to find the
- *     "Actual Text Node", correctly matching the client's expectation for `n1`.
+ *   - `__child(n2, offset)` is used during hydration. It skips the dynamic children
+ *     to find the "Actual Text Node", correctly matching the client's expectation
+ *     for `n1`.
  */
  */
 /*! #__NO_SIDE_EFFECTS__ */
 /*! #__NO_SIDE_EFFECTS__ */
-export function __child(node: ParentNode): Node {
-  let n = node.firstChild!
+export function __child(node: ParentNode, offset?: number): Node {
+  let n = offset ? __nthChild(node, offset) : node.firstChild!
 
 
   if (isComment(n, '[')) {
   if (isComment(n, '[')) {
     n = locateEndAnchor(n)!.nextSibling!
     n = locateEndAnchor(n)!.nextSibling!
@@ -162,8 +162,8 @@ type DelegatedFunction<T extends (...args: any[]) => any> = T & {
 }
 }
 
 
 /*! #__NO_SIDE_EFFECTS__ */
 /*! #__NO_SIDE_EFFECTS__ */
-export const child: DelegatedFunction<typeof _child> = node => {
-  return child.impl(node)
+export const child: DelegatedFunction<typeof __child> = (node, offset) => {
+  return child.impl(node, offset)
 }
 }
 child.impl = _child
 child.impl = _child