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

refactor(runtime-vapor): decouple dynamic fragment attr fallthrough

daiwei 3 недель назад
Родитель
Сommit
d6ddc46986

+ 39 - 0
packages/runtime-vapor/__tests__/if.spec.ts

@@ -224,6 +224,45 @@ describe('createIf', () => {
     expect(frag.scope).toBeUndefined()
   })
 
+  test('should keep branch scope for no-scope branch with fallthrough attrs', async () => {
+    const show = ref(true)
+    const id = ref('a')
+    const t0 = template('<div>foo</div>')
+    let frag!: DynamicFragment
+    const Child = defineVaporComponent({
+      setup() {
+        return (frag = createIf(
+          () => show.value,
+          () => t0(),
+          undefined,
+          singleRootNoScope,
+        ) as DynamicFragment)
+      },
+    })
+
+    const { host } = define(() =>
+      createComponent(Child, { id: () => id.value }, null, true),
+    ).render()
+
+    expect(host.innerHTML).toBe('<div id="a">foo</div><!--if-->')
+    expect(frag.scope).toBeUndefined()
+    expect((frag as any).attrs).toBeUndefined()
+    expect((frag as any).hasFallthroughAttrs).toBe(true)
+
+    id.value = 'b'
+    await nextTick()
+    expect(host.innerHTML).toBe('<div id="b">foo</div><!--if-->')
+
+    show.value = false
+    await nextTick()
+    expect(host.innerHTML).toBe('<!--if-->')
+
+    show.value = true
+    await nextTick()
+    expect(host.innerHTML).toBe('<div id="b">foo</div><!--if-->')
+    expect(frag.scope).toBeDefined()
+  })
+
   test('should skip branch scope for compiler-proven static multi-root branch', async () => {
     const show = ref(true)
     const t0 = template('<div>foo</div>')

+ 23 - 1
packages/runtime-vapor/src/component.ts

@@ -1294,7 +1294,7 @@ function handleSetupResult(
     const root = getRootElement(
       instance.block,
       // attach attrs to root dynamic fragments for applying during each update
-      frag => (frag.attrs = instance.attrs),
+      frag => registerDynamicFragmentFallthroughAttrs(frag, instance.attrs),
       false,
     )
     if (root) {
@@ -1328,3 +1328,25 @@ export function getCurrentScopeId(): string | undefined {
   const scopeOwner = getScopeOwner()
   return scopeOwner ? scopeOwner.type.__scopeId : undefined
 }
+
+function registerDynamicFragmentFallthroughAttrs(
+  frag: DynamicFragment,
+  attrs: Record<string, any>,
+): void {
+  frag.hasFallthroughAttrs = true
+  ;(frag.onBeforeInsert || (frag.onBeforeInsert = [])).push(nodes => {
+    if (nodes instanceof Element) {
+      // ensure render effect is cleaned up when branch scope is stopped
+      frag.scope!.run(() => {
+        renderEffect(() => applyFallthroughProps(nodes, attrs))
+      })
+    } else if (
+      __DEV__ &&
+      // preventing attrs fallthrough on slots
+      // consistent with VDOM slots behavior
+      (frag.anchorLabel === 'slot' || (isArray(nodes) && nodes.length))
+    ) {
+      warnExtraneousAttributes(attrs)
+    }
+  })
+}

+ 8 - 30
packages/runtime-vapor/src/fragment.ts

@@ -21,13 +21,8 @@ import {
   currentInstance,
   queuePostFlushCb,
   setCurrentInstance,
-  warnExtraneousAttributes,
 } from '@vue/runtime-dom'
-import {
-  type VaporComponentInstance,
-  applyFallthroughProps,
-  isVaporComponent,
-} from './component'
+import { type VaporComponentInstance, isVaporComponent } from './component'
 import type { NodeRef } from './apiTemplateRef'
 import {
   advanceHydrationNode,
@@ -45,7 +40,6 @@ import {
   setCurrentHydrationNode,
 } from './dom/hydration'
 import { EMPTY_ARR, isArray } from '@vue/shared'
-import { renderEffect } from './renderEffect'
 import { currentSlotOwner, setCurrentSlotOwner } from './componentSlots'
 import { setBlockKey } from './helpers/setKey'
 import {
@@ -95,6 +89,7 @@ export class VaporFragment<
 
   // hooks
   onBeforeUpdate?: (() => void)[]
+  onBeforeInsert?: ((nodes: Block) => void)[]
   onUpdated?: ((nodes?: Block) => void)[]
 
   // render context
@@ -245,9 +240,8 @@ export class DynamicFragment extends VaporFragment {
   anchorLabel?: string
   keyed?: boolean
   inTransition?: boolean
-
-  // fallthrough attrs
-  attrs?: Record<string, any>
+  // Fallthrough attrs hooks register branch-owned effects on insert.
+  hasFallthroughAttrs?: true
   constructor(
     anchorLabel?: string,
     keyed: boolean = false,
@@ -421,7 +415,7 @@ export class DynamicFragment extends VaporFragment {
       const keepAliveCtx = isKeepAliveEnabled ? this.keepAliveCtx : null
       // A compiler-proven static branch can skip its own EffectScope, but attrs
       // fallthrough still registers branch-owned cleanup.
-      const useScope = !noScope || !!this.attrs
+      const useScope = !noScope || !!this.hasFallthroughAttrs
       if (useScope) {
         // try to reuse the kept-alive scope
         const scope = keepAliveCtx && keepAliveCtx.getScope(this.current)
@@ -473,26 +467,10 @@ export class DynamicFragment extends VaporFragment {
       }
 
       if (parent) {
-        // apply fallthrough props during update
-        if (this.attrs) {
-          if (this.nodes instanceof Element) {
-            // ensure render effect is cleaned up when scope is stopped
-            this.scope!.run(() => {
-              renderEffect(() =>
-                applyFallthroughProps(this.nodes as Element, this.attrs!),
-              )
-            })
-          } else if (
-            __DEV__ &&
-            // preventing attrs fallthrough on slots
-            // consistent with VDOM slots behavior
-            (this.anchorLabel === 'slot' ||
-              (isArray(this.nodes) && this.nodes.length))
-          ) {
-            warnExtraneousAttributes(this.attrs)
-          }
+        const onBeforeInsert = this.onBeforeInsert
+        if (onBeforeInsert) {
+          onBeforeInsert.forEach(hook => hook(this.nodes))
         }
-
         insert(this.nodes, parent, this.anchor)
 
         // For out-in transition, call cacheBlock after renderBranch completes