Преглед изворни кода

fix(runtime-vapor): align interop update hook order with vdom

daiwei пре 3 недеља
родитељ
комит
6c47c4d7e3

+ 1 - 0
packages/runtime-core/src/apiCreateApp.ts

@@ -196,6 +196,7 @@ export interface VaporInteropInterface {
     n2: VNode,
     shouldUpdate: boolean,
     onBeforeUpdate?: () => void,
+    onVnodeBeforeUpdate?: () => void,
   ): void
   unmount(vnode: VNode, doRemove?: boolean): void
   move(vnode: VNode, container: any, anchor: any, moveType: MoveType): void

+ 16 - 3
packages/runtime-core/src/renderer.ts

@@ -1226,19 +1226,32 @@ function baseCreateRenderer(
           }
         }
       } else {
+        const shouldUpdate = shouldUpdateComponent(n1, n2, optimized)
         getVaporInterface(parentComponent, n2).update(
           n1,
           n2,
-          shouldUpdateComponent(n1, n2, optimized),
+          shouldUpdate,
           () => {
             if (n2.dirs) {
               invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
             }
           },
+          () => {
+            const vnodeBeforeUpdateHook =
+              n2.props && n2.props.onVnodeBeforeUpdate
+            if (vnodeBeforeUpdateHook) {
+              invokeVNodeHook(vnodeBeforeUpdateHook, parentComponent, n2, n1)
+            }
+          },
         )
-        if (n2.dirs) {
+        const vnodeUpdatedHook = n2.props && n2.props.onVnodeUpdated
+        if (shouldUpdate && (vnodeUpdatedHook || n2.dirs)) {
           queuePostRenderEffect(
-            () => invokeDirectiveHook(n2, n1, parentComponent, 'updated'),
+            () => {
+              n2.dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
+              vnodeUpdatedHook &&
+                invokeVNodeHook(vnodeUpdatedHook, parentComponent, n2, n1)
+            },
             undefined,
             parentSuspense,
           )

+ 73 - 71
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -319,6 +319,59 @@ describe('vdomInterop', () => {
       await nextTick()
       expect(vnodeUnmounted).toHaveBeenCalledTimes(1)
     })
+
+    test('should invoke update hooks in VDOM order on normal updates', async () => {
+      const msg = ref('foo')
+      const calls: string[] = []
+
+      const vCustom = {
+        beforeUpdate: vi.fn(() => calls.push('directive beforeUpdate')),
+        updated: vi.fn(() => calls.push('directive updated')),
+      }
+
+      const VaporChild = defineVaporComponent({
+        props: {
+          msg: String,
+        },
+        setup(props: any) {
+          const n0 = template('<div> </div>', true)() as any
+          const x0 = child(n0) as any
+          renderEffect(() => {
+            setText(x0, props.msg)
+          })
+          return n0
+        },
+      })
+
+      const App = defineComponent({
+        setup() {
+          return () =>
+            withDirectives(
+              h(VaporChild as any, {
+                msg: msg.value,
+                onVnodeBeforeUpdate: () => calls.push('vnode beforeUpdate'),
+                onVnodeUpdated: () => calls.push('vnode updated'),
+              }),
+              [[vCustom]],
+            )
+        },
+      })
+
+      const root = document.createElement('div')
+      const app = createApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(root)
+
+      msg.value = 'bar'
+      await nextTick()
+
+      expect(calls).toEqual([
+        'vnode beforeUpdate',
+        'directive beforeUpdate',
+        'directive updated',
+        'vnode updated',
+      ])
+    })
   })
 
   describe('v-model', () => {
@@ -2207,7 +2260,7 @@ describe('vdomInterop', () => {
       expect(vnodeMounted).toHaveBeenCalledTimes(2)
     })
 
-    test('should invoke onVnodeBeforeUpdate/onVnodeUpdated on reactivation', async () => {
+    test('should invoke update hooks in VDOM order on reactivation', async () => {
       const VaporChild = defineVaporComponent({
         props: ['msg'],
         setup(props: any) {
@@ -2223,82 +2276,27 @@ describe('vdomInterop', () => {
 
       const current = shallowRef<any>(VaporChild)
       const msg = ref('hello')
-      const beforeUpdateSpy = vi.fn()
-      const updatedSpy = vi.fn()
-
-      const App = defineComponent({
-        setup() {
-          return () =>
-            h(KeepAlive, null, {
-              default: () =>
-                h(
-                  resolveDynamicComponent(current.value) as any,
-                  current.value === VaporChild
-                    ? {
-                        msg: msg.value,
-                        onVnodeBeforeUpdate: beforeUpdateSpy,
-                        onVnodeUpdated: updatedSpy,
-                      }
-                    : null,
-                ),
-            })
-        },
-      })
-
-      const root = document.createElement('div')
-      const app = createApp(App)
-      app.use(vaporInteropPlugin)
-      app.mount(root)
-      await nextTick()
-
-      // Deactivate vapor child
-      current.value = VdomChild
-      await nextTick()
-
-      // Change props while deactivated
-      msg.value = 'world'
-
-      // Reactivate — should trigger update hooks
-      current.value = VaporChild
-      await nextTick()
-      expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
-      expect(updatedSpy).toHaveBeenCalledTimes(1)
-    })
-
-    test('should invoke directive beforeUpdate/updated on reactivation', async () => {
-      const beforeUpdateSpy = vi.fn()
-      const updatedSpy = vi.fn()
-
+      const calls: string[] = []
       const vDir = {
-        beforeUpdate: beforeUpdateSpy,
-        updated: updatedSpy,
+        beforeUpdate: vi.fn(() => calls.push('directive beforeUpdate')),
+        updated: vi.fn(() => calls.push('directive updated')),
       }
 
-      const VaporChild = defineVaporComponent({
-        props: ['msg'],
-        setup(props: any) {
-          return template('<div></div>')()
-        },
-      })
-
-      const VdomChild = defineComponent({
-        setup() {
-          return () => h('span', 'vdom')
-        },
-      })
-
-      const current = shallowRef<any>(VaporChild)
-      const msg = ref('hello')
-
       const App = defineComponent({
         setup() {
           return () =>
             h(KeepAlive, null, {
               default: () =>
                 current.value === VaporChild
-                  ? withDirectives(h(VaporChild as any, { msg: msg.value }), [
-                      [vDir],
-                    ])
+                  ? withDirectives(
+                      h(VaporChild as any, {
+                        msg: msg.value,
+                        onVnodeBeforeUpdate: () =>
+                          calls.push('vnode beforeUpdate'),
+                        onVnodeUpdated: () => calls.push('vnode updated'),
+                      }),
+                      [[vDir]],
+                    )
                   : h(VdomChild),
             })
         },
@@ -2317,11 +2315,15 @@ describe('vdomInterop', () => {
       // Change props while deactivated
       msg.value = 'world'
 
-      // Reactivate — should trigger directive update hooks
+      // Reactivate — should trigger update hooks
       current.value = VaporChild
       await nextTick()
-      expect(beforeUpdateSpy).toHaveBeenCalledTimes(1)
-      expect(updatedSpy).toHaveBeenCalledTimes(1)
+      expect(calls).toEqual([
+        'vnode beforeUpdate',
+        'directive beforeUpdate',
+        'directive updated',
+        'vnode updated',
+      ])
     })
 
     test('should bail out directive beforeUpdate/updated on reactivation for non-element root vapor child', async () => {

+ 17 - 16
packages/runtime-vapor/src/vdomInterop.ts

@@ -247,26 +247,27 @@ const vaporInteropImpl: Omit<
     return instance
   },
 
-  update(n1, n2, shouldUpdate, onBeforeUpdate) {
+  update(n1, n2, shouldUpdate, onBeforeUpdate, onVnodeBeforeUpdate) {
     n2.component = n1.component
     n2.el = n2.anchor = n1.anchor
 
     const instance = n2.component as any as VaporComponentInstance
 
-    const rootEl = getRootElement(instance)
-    if (rootEl) {
-      n2.el = rootEl
-    }
-    // invoke directive hooks only when we have a valid root element
-    if (n2.dirs) {
+    if (shouldUpdate) {
+      const rootEl = getRootElement(instance)
       if (rootEl) {
-        onBeforeUpdate && onBeforeUpdate()
-      } else {
-        n2.dirs = null
+        n2.el = rootEl
+      }
+      // align with VDOM: vnode beforeUpdate runs before directive beforeUpdate.
+      onVnodeBeforeUpdate && onVnodeBeforeUpdate()
+      // invoke directive hooks only when we have a valid root element
+      if (n2.dirs) {
+        if (rootEl) {
+          onBeforeUpdate && onBeforeUpdate()
+        } else {
+          n2.dirs = null
+        }
       }
-    }
-
-    if (shouldUpdate) {
       instance.rawPropsRef!.value = filterReservedProps(n2.props)
       instance.rawSlotsRef!.value = n2.children
     }
@@ -424,9 +425,6 @@ const vaporInteropImpl: Omit<
     if (shouldUpdate) {
       instance.rawPropsRef!.value = filterReservedProps(vnode.props)
       instance.rawSlotsRef!.value = vnode.children
-      if (vnode.dirs) {
-        invokeDirectiveHook(vnode, cached, parentComponent, 'beforeUpdate')
-      }
       const vnodeBeforeUpdateHook =
         vnode.props && vnode.props.onVnodeBeforeUpdate
       if (vnodeBeforeUpdateHook) {
@@ -437,6 +435,9 @@ const vaporInteropImpl: Omit<
           [vnode, cached],
         )
       }
+      if (vnode.dirs) {
+        invokeDirectiveHook(vnode, cached, parentComponent, 'beforeUpdate')
+      }
     }
     queuePostFlushCb(() => {
       if (shouldUpdate) {