فهرست منبع

fix(runtime-vapor): sync vnode hooks and el on interop self-update

daiwei 3 هفته پیش
والد
کامیت
820b4700c7
2فایلهای تغییر یافته به همراه133 افزوده شده و 4 حذف شده
  1. 58 0
      packages/runtime-vapor/__tests__/vdomInterop.spec.ts
  2. 75 4
      packages/runtime-vapor/src/vdomInterop.ts

+ 58 - 0
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -12,9 +12,11 @@ import {
   nextTick,
   nextTick,
   onActivated,
   onActivated,
   onBeforeMount,
   onBeforeMount,
+  onBeforeUpdate,
   onDeactivated,
   onDeactivated,
   onMounted,
   onMounted,
   onUnmounted,
   onUnmounted,
+  onUpdated,
   provide,
   provide,
   ref,
   ref,
   renderSlot,
   renderSlot,
@@ -3207,5 +3209,61 @@ describe('vdomInterop', () => {
       await nextTick()
       await nextTick()
       expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
       expect(beforeUnmountSpy).toHaveBeenCalledTimes(1)
     })
     })
+
+    test('should invoke update hooks in VDOM order on vapor child self-update', async () => {
+      const calls: string[] = []
+      let flip!: () => void
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          const alt = ref(false)
+          onBeforeUpdate(() => calls.push('component beforeUpdate'))
+          onUpdated(() => calls.push('component updated'))
+          flip = () => {
+            alt.value = true
+          }
+          return createIf(
+            () => alt.value,
+            () => template('<p>alt</p>')(),
+            () => template('<div>base</div>')(),
+          )
+        },
+      })
+
+      const App = defineComponent({
+        setup() {
+          return () =>
+            h(VaporChild as any, {
+              onVnodeBeforeUpdate: (vnode: any, prevVNode: any) => {
+                expect((vnode.el as Element).tagName).toBe('DIV')
+                expect((prevVNode.el as Element).tagName).toBe('DIV')
+                calls.push('vnode beforeUpdate')
+              },
+              onVnodeUpdated: (vnode: any) => {
+                expect((vnode.el as Element).tagName).toBe('P')
+                calls.push('vnode updated')
+              },
+            })
+        },
+      })
+
+      const root = document.createElement('div')
+      const app = createApp(App)
+      app.use(vaporInteropPlugin)
+      app.mount(root)
+
+      expect(root.querySelector('div')!.textContent).toBe('base')
+
+      flip()
+      await nextTick()
+
+      expect(root.querySelector('p')!.textContent).toBe('alt')
+      expect(calls).toEqual([
+        'component beforeUpdate',
+        'vnode beforeUpdate',
+        'component updated',
+        'vnode updated',
+      ])
+    })
   })
   })
 })
 })

+ 75 - 4
packages/runtime-vapor/src/vdomInterop.ts

@@ -184,6 +184,7 @@ const vaporInteropImpl: Omit<
     ))
     ))
     instance.rawPropsRef = propsRef
     instance.rawPropsRef = propsRef
     instance.rawSlotsRef = slotsRef
     instance.rawSlotsRef = slotsRef
+    ensureVNodeHookState(instance, vnode)
 
 
     // copy the shape flag from the vdom component if inside a keep-alive
     // copy the shape flag from the vdom component if inside a keep-alive
     if (isKeepAlive(parentComponent)) instance.shapeFlag = vnode.shapeFlag
     if (isKeepAlive(parentComponent)) instance.shapeFlag = vnode.shapeFlag
@@ -232,6 +233,7 @@ const vaporInteropImpl: Omit<
     n2.anchor = n1.anchor
     n2.anchor = n1.anchor
 
 
     const instance = n2.component as any as VaporComponentInstance
     const instance = n2.component as any as VaporComponentInstance
+    const vnodeHookState = ensureVNodeHookState(instance, n2)
 
 
     if (shouldUpdate) {
     if (shouldUpdate) {
       const rootEl = getRootElement(instance)
       const rootEl = getRootElement(instance)
@@ -248,9 +250,15 @@ const vaporInteropImpl: Omit<
           n2.dirs = null
           n2.dirs = null
         }
         }
       }
       }
+      vnodeHookState.skipVnodeHooks = true
       instance.rawPropsRef!.value = filterReservedProps(n2.props)
       instance.rawPropsRef!.value = filterReservedProps(n2.props)
       instance.rawSlotsRef!.value = n2.children
       instance.rawSlotsRef!.value = n2.children
-      queuePostFlushCb(() => syncVNodeRootEl(n2, instance))
+      queuePostFlushCb(() => {
+        syncVNodeEl(n2, instance)
+        if (!instance.isUpdating) {
+          vnodeHookState.skipVnodeHooks = false
+        }
+      })
     }
     }
   },
   },
 
 
@@ -392,6 +400,7 @@ const vaporInteropImpl: Omit<
     vnode.component = cached.component
     vnode.component = cached.component
     vnode.anchor = cached.anchor
     vnode.anchor = cached.anchor
     const instance = vnode.component as any as VaporComponentInstance
     const instance = vnode.component as any as VaporComponentInstance
+    const vnodeHookState = ensureVNodeHookState(instance, vnode)
     const rootEl = getRootElement(instance)
     const rootEl = getRootElement(instance)
     if (rootEl) {
     if (rootEl) {
       vnode.el = rootEl
       vnode.el = rootEl
@@ -407,6 +416,7 @@ const vaporInteropImpl: Omit<
     }
     }
     const shouldUpdate = shouldUpdateComponent(cached, vnode)
     const shouldUpdate = shouldUpdateComponent(cached, vnode)
     if (shouldUpdate) {
     if (shouldUpdate) {
+      vnodeHookState.skipVnodeHooks = true
       instance.rawPropsRef!.value = filterReservedProps(vnode.props)
       instance.rawPropsRef!.value = filterReservedProps(vnode.props)
       instance.rawSlotsRef!.value = vnode.children
       instance.rawSlotsRef!.value = vnode.children
       const vnodeBeforeUpdateHook =
       const vnodeBeforeUpdateHook =
@@ -423,7 +433,7 @@ const vaporInteropImpl: Omit<
         invokeDirectiveHook(vnode, cached, parentComponent, 'beforeUpdate')
         invokeDirectiveHook(vnode, cached, parentComponent, 'beforeUpdate')
       }
       }
       queuePostFlushCb(() => {
       queuePostFlushCb(() => {
-        syncVNodeRootEl(vnode, instance)
+        syncVNodeEl(vnode, instance)
         if (vnode.dirs) {
         if (vnode.dirs) {
           invokeDirectiveHook(vnode, cached, parentComponent, 'updated')
           invokeDirectiveHook(vnode, cached, parentComponent, 'updated')
         }
         }
@@ -436,6 +446,9 @@ const vaporInteropImpl: Omit<
             [vnode, cached],
             [vnode, cached],
           )
           )
         }
         }
+        if (!instance.isUpdating) {
+          vnodeHookState.skipVnodeHooks = false
+        }
       })
       })
     }
     }
     activate(instance, container, anchor)
     activate(instance, container, anchor)
@@ -1270,12 +1283,70 @@ function invokeVaporSlot(vnode: VNode): Block {
   }
   }
 }
 }
 
 
-function syncVNodeRootEl(vnode: VNode, instance: VaporComponentInstance): void {
+function syncVNodeEl(vnode: VNode, instance: VaporComponentInstance): void {
   const rootEl = getRootElement(instance)
   const rootEl = getRootElement(instance)
   if (rootEl) {
   if (rootEl) {
     vnode.el = rootEl
     vnode.el = rootEl
   } else {
   } else {
-    vnode.el = vnode.anchor as any
+    vnode.el = vnode.anchor
     vnode.dirs = null
     vnode.dirs = null
   }
   }
 }
 }
+
+interface VNodeHookState {
+  vnode: VNode
+  skipVnodeHooks: boolean
+}
+
+const vnodeHookStateMap = new WeakMap<VaporComponentInstance, VNodeHookState>()
+
+function ensureVNodeHookState(
+  instance: VaporComponentInstance,
+  vnode: VNode,
+): VNodeHookState {
+  let state = vnodeHookStateMap.get(instance)
+  if (!state) {
+    state = {
+      vnode,
+      skipVnodeHooks: false,
+    }
+    vnodeHookStateMap.set(instance, state)
+    ;(instance.bu || (instance.bu = [])).push(() => {
+      if (state!.skipVnodeHooks) return
+      const vnodeHook =
+        state!.vnode.props && state!.vnode.props.onVnodeBeforeUpdate
+      if (vnodeHook) {
+        callWithAsyncErrorHandling(
+          vnodeHook,
+          instance.parent,
+          ErrorCodes.VNODE_HOOK,
+          [state!.vnode, state!.vnode],
+        )
+      }
+    })
+
+    // Sync the outer component vnode before running any updated hooks so
+    // both component updated hooks and onVnodeUpdated see the latest root el.
+    ;(instance.u || (instance.u = [])).unshift(() =>
+      syncVNodeEl(state!.vnode, instance),
+    )
+    instance.u.push(() => {
+      if (state!.skipVnodeHooks) {
+        state!.skipVnodeHooks = false
+        return
+      }
+      const vnodeHook = state!.vnode.props && state!.vnode.props.onVnodeUpdated
+      if (vnodeHook) {
+        callWithAsyncErrorHandling(
+          vnodeHook,
+          instance.parent,
+          ErrorCodes.VNODE_HOOK,
+          [state!.vnode, state!.vnode],
+        )
+      }
+    })
+  } else {
+    state.vnode = vnode
+  }
+  return state
+}