]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: refactor hydration for v-if
authordaiwei <daiwei521@126.com>
Thu, 24 Apr 2025 08:58:18 +0000 (16:58 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 24 Apr 2025 14:12:29 +0000 (22:12 +0800)
17 files changed:
packages/compiler-ssr/__tests__/ssrComponent.spec.ts
packages/compiler-ssr/__tests__/ssrFallthroughAttrs.spec.ts
packages/compiler-ssr/__tests__/ssrInjectCssVars.spec.ts
packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
packages/compiler-ssr/__tests__/ssrTransitionGroup.spec.ts
packages/compiler-ssr/__tests__/ssrVIf.spec.ts
packages/compiler-ssr/__tests__/ssrVModel.spec.ts
packages/compiler-ssr/src/ssrCodegenTransform.ts
packages/compiler-ssr/src/transforms/ssrVIf.ts
packages/runtime-core/__tests__/hydration.spec.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-vapor/__tests__/hydration.spec.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/dom/hydration.ts
packages/runtime-vapor/src/dom/node.ts

index 2fde4560ec43e4e81dfc0848e4ac5aa4c4d14ccb..7459cb4929f6bea2944026440a69a353d3112b5b 100644 (file)
@@ -245,6 +245,7 @@ describe('ssr: components', () => {
                     _push(\`<span\${_scopeId}></span>\`)
                   })
                   _push(\`<!--]--></div>\`)
+                  _push(\`<!--$-->\`)
                 } else {
                   _push(\`<!---->\`)
                 }
@@ -268,6 +269,7 @@ describe('ssr: components', () => {
                     _push(\`<span\${_scopeId}></span>\`)
                   })
                   _push(\`<!--]--></div>\`)
+                  _push(\`<!--$-->\`)
                 } else {
                   _push(\`<!---->\`)
                 }
@@ -361,6 +363,7 @@ describe('ssr: components', () => {
                   _push(\`\`)
                   if (false) {
                     _push(\`<div\${_scopeId}></div>\`)
+                    _push(\`<!--$-->\`)
                   } else {
                     _push(\`<!---->\`)
                   }
index 7b3d1962c3e0947f30acd2d960870d69bbd4a460..a837979003a4e36d40be4c46b812e8052929b51e 100644 (file)
@@ -29,6 +29,7 @@ describe('ssr: attrs fallthrough', () => {
         _push(\`<!--[-->\`)
         if (true) {
           _push(\`<div></div>\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!---->\`)
         }
index 9e70dac0bdca3cc1f535def98297eddd0a1692e2..3eef5a7771da4adce31176b6d7991773ef3f7681 100644 (file)
@@ -70,6 +70,7 @@ describe('ssr: inject <style vars>', () => {
         const _cssVars = { style: { color: _ctx.color }}
         if (_ctx.ok) {
           _push(\`<div\${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}></div>\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!--[--><div\${
             _ssrRenderAttrs(_cssVars)
index 86863cfb85f07d85d2b647fc2118d6e75f0d877a..95f48b3b0686fbc2c5d452e94136a0cb9eea3902 100644 (file)
@@ -153,6 +153,7 @@ describe('ssr: <slot>', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (true) {
           _ssrRenderSlotInner(_ctx.$slots, "default", {}, null, _push, _parent, null, true)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!---->\`)
         }
index 82122e621c7b5890a9160172f51bba692086b270..7111dd5a2717281d6ff777b51c8a4d331d27b011 100644 (file)
@@ -54,6 +54,7 @@ describe('transition-group', () => {
         })
         if (false) {
           _push(\`<div></div>\`)
+          _push(\`<!--$-->\`)
         }
         _push(\`</ul>\`)
       }"
@@ -123,6 +124,7 @@ describe('transition-group', () => {
         })
         if (_ctx.ok) {
           _push(\`<div>ok</div>\`)
+          _push(\`<!--$-->\`)
         }
         _push(\`<!--]-->\`)
       }"
index b544adadcf3a77293cbf91b35b48f3305707ea0d..d9ee698882121aef418619633b8e32d0a4a9ea40 100644 (file)
@@ -8,6 +8,7 @@ describe('ssr: v-if', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (_ctx.foo) {
           _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -23,6 +24,7 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}>hello<span>ok</span></div>\`)
+            _push(\`<!--$-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -38,6 +40,7 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+            _push(\`<!--$-->\`)
           } else {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
           }
@@ -53,8 +56,10 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+            _push(\`<!--$-->\`)
           } else if (_ctx.bar) {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
+            _push(\`<!--$-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -70,8 +75,10 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
+            _push(\`<!--$-->\`)
           } else if (_ctx.bar) {
             _push(\`<span\${_ssrRenderAttrs(_attrs)}></span>\`)
+            _push(\`<!--$-->\`)
           } else {
             _push(\`<p\${_ssrRenderAttrs(_attrs)}></p>\`)
           }
@@ -82,15 +89,16 @@ describe('ssr: v-if', () => {
   test('<template v-if> (text)', () => {
     expect(compile(`<template v-if="foo">hello</template>`).code)
       .toMatchInlineSnapshot(`
-      "
-      return function ssrRender(_ctx, _push, _parent, _attrs) {
-        if (_ctx.foo) {
-          _push(\`<!--[-->hello<!--]-->\`)
-        } else {
-          _push(\`<!---->\`)
-        }
-      }"
-    `)
+        "
+        return function ssrRender(_ctx, _push, _parent, _attrs) {
+          if (_ctx.foo) {
+            _push(\`<!--[-->hello<!--]-->\`)
+            _push(\`<!--$-->\`)
+          } else {
+            _push(\`<!---->\`)
+          }
+        }"
+      `)
   })
 
   test('<template v-if> (single element)', () => {
@@ -102,6 +110,7 @@ describe('ssr: v-if', () => {
         return function ssrRender(_ctx, _push, _parent, _attrs) {
           if (_ctx.foo) {
             _push(\`<div\${_ssrRenderAttrs(_attrs)}>hi</div>\`)
+            _push(\`<!--$-->\`)
           } else {
             _push(\`<!---->\`)
           }
@@ -118,6 +127,7 @@ describe('ssr: v-if', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (_ctx.foo) {
           _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -138,6 +148,7 @@ describe('ssr: v-if', () => {
             _push(\`<div></div>\`)
           })
           _push(\`<!--]-->\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!---->\`)
         }
@@ -156,6 +167,7 @@ describe('ssr: v-if', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         if (_ctx.foo) {
           _push(\`<!--[--><div>hi</div><div>ho</div><!--]-->\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<div\${_ssrRenderAttrs(_attrs)}></div>\`)
         }
index 0bf7673d00dc1c3bc3a6daabe80a9b079020719f..906c62df21abfee0342c40c8446b04cc036ed95f 100644 (file)
@@ -91,6 +91,7 @@ describe('ssr: v-model', () => {
               ? _ssrLooseContain(_ctx.model, _ctx.i)
               : _ssrLooseEqual(_ctx.model, _ctx.i))) ? " selected" : ""
           }></option>\`)
+          _push(\`<!--$-->\`)
         } else {
           _push(\`<!---->\`)
         }
index 14ea4177dd45bccda466d360aeb9f0be8850f563..8a98c2422783ad4e7fd0faacbc630ddf1bdd5e0f 100644 (file)
@@ -295,8 +295,13 @@ function processChildrenDynamicInfo(
 
   for (let i = 0; i < filteredChildren.length; i++) {
     const child = filteredChildren[i]
-    if (isStaticChildNode(child)) continue
-
+    if (
+      isStaticChildNode(child) ||
+      // v-if has an anchor, which can be used to distinguish the boundary
+      child.type === NodeTypes.IF
+    ) {
+      continue
+    }
     child._ssrDynamicInfo = {
       hasStaticPrevious: false,
       hasStaticNext: false,
index 0e3880247a16e29b5ecb2b187be71d2ae0478e26..74ac319f4c338653fd6680d23c8eacc356afa88b 100644 (file)
@@ -74,5 +74,13 @@ function processIfBranch(
     (children.length !== 1 || children[0].type !== NodeTypes.ELEMENT) &&
     // optimize away nested fragments when the only child is a ForNode
     !(children.length === 1 && children[0].type === NodeTypes.FOR)
-  return processChildrenAsStatement(branch, context, needFragmentWrapper)
+  const statement = processChildrenAsStatement(
+    branch,
+    context,
+    needFragmentWrapper,
+  )
+  if (branch.condition) {
+    statement.body.push(createCallExpression(`_push`, ['`<!--$-->`']))
+  }
+  return statement
 }
index 43d536e6d596e247c3826d2aeb48721b7dc3f50d..9cf6870c479f777717446dc612d7731e479f786c 100644 (file)
@@ -598,14 +598,14 @@ describe('SSR hydration', () => {
     const ctx: SSRContext = {}
     container.innerHTML = await renderToString(h(App), ctx)
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--></div>',
+      '<div><!--teleport start--><!--teleport end--><!--$--></div>',
     )
     teleportContainer.innerHTML = ctx.teleports!['#target']
 
     // hydrate
     createSSRApp(App).mount(container)
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--></div>',
+      '<div><!--teleport start--><!--teleport end--><!--$--></div>',
     )
     expect(teleportContainer.innerHTML).toBe(
       '<!--teleport start anchor--><span>Teleported Comp1</span><!--teleport anchor-->',
@@ -614,7 +614,7 @@ describe('SSR hydration', () => {
 
     toggle.value = false
     await nextTick()
-    expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+    expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
     expect(teleportContainer.innerHTML).toBe('')
   })
 
@@ -657,21 +657,21 @@ describe('SSR hydration', () => {
     // server render
     container.innerHTML = await renderToString(h(App))
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--></div>',
+      '<div><!--teleport start--><!--teleport end--><!--$--></div>',
     )
     expect(teleportContainer.innerHTML).toBe('')
 
     // hydrate
     createSSRApp(App).mount(container)
     expect(container.innerHTML).toBe(
-      '<div><!--teleport start--><!--teleport end--></div>',
+      '<div><!--teleport start--><!--teleport end--><!--$--></div>',
     )
     expect(teleportContainer.innerHTML).toBe('<span>Teleported Comp1</span>')
     expect(`Hydration children mismatch`).toHaveBeenWarned()
 
     toggle.value = false
     await nextTick()
-    expect(container.innerHTML).toBe('<div><div>Comp2</div></div>')
+    expect(container.innerHTML).toBe('<div><div>Comp2</div><!--$--></div>')
     expect(teleportContainer.innerHTML).toBe('')
   })
 
index f0fc99ad2ff69604f1ae32bcf7641a95de593f14..876fd877eda872871fdd4d65610e35f2add40eab 100644 (file)
@@ -84,10 +84,14 @@ const getContainerType = (
   return undefined
 }
 
-export function isDynamicAnchor(node: Node): boolean {
+export function isDynamicAnchor(node: Node): node is Comment {
   return isComment(node) && (node.data === '[[' || node.data === ']]')
 }
 
+export function isDynamicFragmentEndAnchor(node: Node): node is Comment {
+  return isComment(node) && node.data === '$'
+}
+
 export const isComment = (node: Node): node is Comment =>
   node.nodeType === DOMNodeTypes.COMMENT
 
@@ -125,8 +129,10 @@ export function createHydrationFunctions(
 
   function nextSibling(node: Node) {
     let n = next(node)
-    // skip dynamic anchors
-    if (n && isDynamicAnchor(n)) {
+    // skip if:
+    // - dynamic anchors (`<!--[-->`, `<!--]-->`)
+    // - dynamic fragment end anchors (`<!--$-->`)
+    if (n && (isDynamicAnchor(n) || isDynamicFragmentEndAnchor(n))) {
       n = next(n)
     }
     return n
@@ -158,7 +164,9 @@ export function createHydrationFunctions(
     slotScopeIds: string[] | null,
     optimized = false,
   ): Node | null => {
-    if (isDynamicAnchor(node)) node = nextSibling(node)!
+    if (isDynamicAnchor(node) || isDynamicFragmentEndAnchor(node)) {
+      node = nextSibling(node)!
+    }
     optimized = optimized || !!vnode.dynamicChildren
     const isFragmentStart = isComment(node) && node.data === '['
     const onMismatch = () =>
index 4c31ff510e034eaea67f4e315b46ce867a5290f3..b2e2dfcf1807405eeaa9234596dc440b911ce36a 100644 (file)
@@ -560,4 +560,4 @@ export { initFeatureFlags } from './featureFlags'
 /**
  * @internal
  */
-export { isDynamicAnchor } from './hydration'
+export { isDynamicAnchor, isDynamicFragmentEndAnchor } from './hydration'
index b02f50177a40b722aaad9dc2273b8ecc87b2c136..f67a4c4202e123575454f84594925b1ecce70649 100644 (file)
@@ -730,7 +730,97 @@ describe('Vapor Mode hydration', () => {
       expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
     })
 
-    test.todo('on component', async () => {})
+    test('on component', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <components.Child v-if="data"/>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toMatchInlineSnapshot(`"foo<!--if-->"`)
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toMatchInlineSnapshot(`"<!--if-->"`)
+    })
+
+    test('on component with anchor insertion', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><span></span>foo<!--if--><span></span></div>"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><span></span><!--if--><span></span></div>"`,
+      )
+    })
+
+    test('consecutive v-if on component with anchor insertion', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+          <div>
+            <span/>
+            <components.Child v-if="data"/>
+            <components.Child v-if="data"/>
+            <span/>
+          </div>
+        </template>`,
+        { Child: `<template>foo</template>` },
+        data,
+      )
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><span></span>foo<!--if-->foo<!--if--><span></span></div>"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><span></span><!--if--><!--if--><span></span></div>"`,
+      )
+    })
+
+    test('consecutive v-if on fragment component with anchor insertion', async () => {
+      const data = ref(true)
+      const { container } = await testHydration(
+        `<template>
+            <div>
+              <span/>
+              <components.Child v-if="data"/>
+              <components.Child v-if="data"/>
+              <span/>
+            </div>
+          </template>`,
+        {
+          Child: `<template><div>{{ data }}</div>-{{ data }}-</template>`,
+        },
+        data,
+      )
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><span></span><!--[--><div>true</div>-true-<!--]--><!--if--><!--[--><div>true</div>-true-<!--]--><!--if--><span></span></div>"`,
+      )
+
+      data.value = false
+      await nextTick()
+      expect(container.innerHTML).toMatchInlineSnapshot(
+        `"<div><span></span><!--[--><!--]--><!--if--><!--[--><!--]--><!--if--><span></span></div>"`,
+      )
+    })
   })
 
   test.todo('for')
index 6210e9221b18d3fe7c8f9b36f0c385f01d0e0372..1e81d3745a743917d38e335f3f29aaedadce3577 100644 (file)
@@ -1,10 +1,5 @@
 import { type Block, type BlockFn, DynamicFragment, insert } from './block'
-import {
-  currentHydrationNode,
-  isComment,
-  isHydrating,
-  locateHydrationNode,
-} from './dom/hydration'
+import { isHydrating, locateHydrationNode } from './dom/hydration'
 import { insertionAnchor, insertionParent } from './insertionState'
 import { renderEffect } from './renderEffect'
 
@@ -16,10 +11,8 @@ export function createIf(
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
-  let _currentHydrationNode
   if (isHydrating) {
-    locateHydrationNode()
-    _currentHydrationNode = currentHydrationNode
+    locateHydrationNode(true)
   }
 
   let frag: Block
@@ -34,23 +27,5 @@ export function createIf(
     insert(frag, _insertionParent, _insertionAnchor)
   }
 
-  // if the current hydration node is a comment, use it as an anchor
-  // otherwise need to insert the anchor node
-  // OR adjust ssr output to add anchor for v-if
-  else if (isHydrating && _currentHydrationNode) {
-    const parentNode = _currentHydrationNode.parentNode
-    if (parentNode) {
-      if (isComment(_currentHydrationNode, '')) {
-        if (__DEV__) _currentHydrationNode.data = 'if'
-        ;(frag as DynamicFragment).anchor = _currentHydrationNode
-      } else {
-        parentNode.insertBefore(
-          (frag as DynamicFragment).anchor,
-          _currentHydrationNode.nextSibling,
-        )
-      }
-    }
-  }
-
   return frag
 }
index b782afd38d35b66c9fed33675d4f4705efbceb3f..36d0bc387b7ef29a9990f5d8120cd935459879ca 100644 (file)
@@ -5,9 +5,10 @@ import {
   mountComponent,
   unmountComponent,
 } from './component'
-import { createComment, createTextNode } from './dom/node'
+import { createComment, createTextNode, next } from './dom/node'
 import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
-import { isHydrating } from './dom/hydration'
+import { currentHydrationNode, isComment, isHydrating } from './dom/hydration'
+import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
 
 export type Block =
   | Node
@@ -30,15 +31,19 @@ export class VaporFragment {
 }
 
 export class DynamicFragment extends VaporFragment {
-  anchor: Node
+  anchor!: Node
   scope: EffectScope | undefined
   current?: BlockFn
   fallback?: BlockFn
 
   constructor(anchorLabel?: string) {
     super([])
-    this.anchor =
-      __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+    if (isHydrating) {
+      this.hydrate(anchorLabel)
+    } else {
+      this.anchor =
+        __DEV__ && anchorLabel ? createComment(anchorLabel) : createTextNode()
+    }
   }
 
   update(render?: BlockFn, key: any = render): void {
@@ -75,6 +80,24 @@ export class DynamicFragment extends VaporFragment {
 
     resetTracking()
   }
+
+  hydrate(label?: string): void {
+    // for v-if="false" the hydrationNode will be a empty comment node
+    // use it as anchor.
+    // otherwise, use the next sibling comment node as anchor
+    if (isComment(currentHydrationNode!, '')) {
+      this.anchor = currentHydrationNode
+    } else {
+      const anchor = next(currentHydrationNode!)
+      if (isDynamicFragmentEndAnchor(anchor)) {
+        this.anchor = anchor
+      } else if (__DEV__) {
+        // TODO warning
+        warn(`DynamicFragment anchor not found...`)
+      }
+    }
+    if (__DEV__ && label) (this.anchor as Comment).data = label
+  }
 }
 
 export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
index 55dd9853294e54ad706c6281509a7584187f3c01..4d4ba85c4c616f54083169703ccc0f3236f92dae 100644 (file)
@@ -1,4 +1,4 @@
-import { warn } from '@vue/runtime-dom'
+import { isDynamicFragmentEndAnchor, warn } from '@vue/runtime-dom'
 import {
   insertionAnchor,
   insertionParent,
@@ -10,6 +10,7 @@ import {
   disableHydrationNodeLookup,
   enableHydrationNodeLookup,
   next,
+  prev,
 } from './node'
 
 export let isHydrating = false
@@ -41,7 +42,7 @@ export function withHydration(container: ParentNode, fn: () => void): void {
 }
 
 export let adoptTemplate: (node: Node, template: string) => Node | null
-export let locateHydrationNode: () => void
+export let locateHydrationNode: (isFragment?: boolean) => void
 
 type Anchor = Comment & {
   // cached matching fragment start to avoid repeated traversal
@@ -82,7 +83,7 @@ function adoptTemplateImpl(node: Node, template: string): Node | null {
   return node
 }
 
-function locateHydrationNodeImpl() {
+function locateHydrationNodeImpl(isFragment?: boolean) {
   let node: Node | null
   // prepend / firstChild
   if (insertionAnchor === 0) {
@@ -92,6 +93,14 @@ function locateHydrationNodeImpl() {
     node = insertionAnchor
   } else {
     node = insertionParent ? insertionParent.lastChild : currentHydrationNode
+
+    // if the last child is a comment, it is the anchor for the fragment
+    // so it need to find the previous node
+    if (isFragment && node && isDynamicFragmentEndAnchor(node)) {
+      let previous = prev(node)
+      if (previous) node = previous
+    }
+
     if (node && isComment(node, ']')) {
       // fragment backward search
       if (node.$fs) {
@@ -157,3 +166,25 @@ export function locateEndAnchor(
   }
   return null
 }
+
+export function locateStartAnchor(
+  node: Node | null,
+  open = '[',
+  close = ']',
+): Node | null {
+  let match = 0
+  while (node) {
+    if (node.nodeType === 8) {
+      if ((node as Comment).data === close) match++
+      if ((node as Comment).data === open) {
+        if (match === 0) {
+          return node
+        } else {
+          match--
+        }
+      }
+    }
+    node = node.previousSibling
+  }
+  return null
+}
index 82971e876db345284e9827e132b17bdd4086f6e0..0740c1f7c9218d6fe214d55c3d56c6d97e0a8ee7 100644 (file)
@@ -1,5 +1,10 @@
 import { isDynamicAnchor } from '@vue/runtime-dom'
-import { isComment, isEmptyText, locateEndAnchor } from './hydration'
+import {
+  isComment,
+  isEmptyText,
+  locateEndAnchor,
+  locateStartAnchor,
+} from './hydration'
 
 /*! #__NO_SIDE_EFFECTS__ */
 export function createTextNode(value = ''): Text {
@@ -43,21 +48,17 @@ function _next(node: Node): Node {
 /*! #__NO_SIDE_EFFECTS__ */
 function __next(node: Node): Node {
   // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
-  if (node && isComment(node, '[[')) {
+  if (isComment(node, '[[')) {
     node = locateEndAnchor(node, '[[', ']]')!
   }
 
   // treat dynamic node (<!--[-->...<!--]-->) as a single node
-  else if (node && isComment(node, '[')) {
+  else if (isComment(node, '[')) {
     node = locateEndAnchor(node)!
   }
 
   let n = node.nextSibling!
-  // skip if:
-  // - dynamic anchors (<!--[[-->, <!--]]-->)
-  // - fragment end anchor (`<!--]-->`)
-  // - empty text nodes
-  while (n && (isDynamicAnchor(n) || isComment(n, ']') || isEmptyText(n))) {
+  while (n && isNonHydrationNode(n)) {
     n = n.nextSibling!
   }
   return n
@@ -105,3 +106,48 @@ export function disableHydrationNodeLookup(): void {
   next.impl = _next
   nthChild.impl = _nthChild
 }
+
+/*! #__NO_SIDE_EFFECTS__ */
+export function prev(node: Node): Node | null {
+  // treat dynamic node (<!--[[-->...<!--]]-->) as a single node
+  if (isComment(node, ']]')) {
+    node = locateStartAnchor(node, '[[', ']]')!
+  }
+
+  // treat dynamic node (<!--[-->...<!--]-->) as a single node
+  else if (isComment(node, ']')) {
+    node = locateStartAnchor(node)!
+  }
+
+  let n = node.previousSibling
+  while (n && isNonHydrationNode(n)) {
+    n = n.previousSibling
+  }
+  return n
+}
+
+function isNonHydrationNode(node: Node) {
+  return (
+    // empty text nodes, no need to hydrate
+    isEmptyText(node) ||
+    // dynamic anchors (<!--[[-->, <!--]]-->)
+    isDynamicAnchor(node) ||
+    // fragment end anchor (`<!--]-->`)
+    isComment(node, ']') ||
+    isDynamicFragmentAnchor(node)
+  )
+}
+
+function isDynamicFragmentAnchor(node: Node) {
+  return __DEV__
+    ? // v-if anchor (`<!--if-->`)
+      isComment(node, 'if') ||
+        // v-for anchor (`<!--for-->`)
+        isComment(node, 'for') ||
+        // v-slot anchor (`<!--slot-->`)
+        isComment(node, 'slot') ||
+        // dynamic-component anchor (`<!--dynamic-component-->`)
+        isComment(node, 'dynamic-component')
+    : // TODO ?
+      isComment(node, '$')
+}