diff --git a/packages/runtime-core/__tests__/renderVnodeHook.spec.ts b/packages/runtime-core/__tests__/renderVnodeHook.spec.ts new file mode 100644 index 00000000000..16576a3998d --- /dev/null +++ b/packages/runtime-core/__tests__/renderVnodeHook.spec.ts @@ -0,0 +1,79 @@ +import { h } from '../src/h' +import { nextTick, nodeOps, ref, render } from '@vue/runtime-test' + +describe('renderer: vnode hook', () => { + test('element', async () => { + const onVnodeBeforeMount = jest.fn() + const onVnodeMounted = jest.fn() + const onVnodeBeforeUpdate = jest.fn() + const onVnodeUpdated = jest.fn() + const onVnodeBeforeUnmount = jest.fn() + const onVnodeUnmounted = jest.fn() + const root = nodeOps.createElement('div') + const count = ref(0) + + const App = () => + h('div', { + onVnodeBeforeMount, + onVnodeMounted, + onVnodeBeforeUpdate, + onVnodeUpdated, + onVnodeBeforeUnmount, + onVnodeUnmounted, + count: count.value + }) + + render(h(App), root) + await nextTick() + expect(onVnodeBeforeMount).toBeCalled() + expect(onVnodeMounted).toBeCalled() + + count.value++ + await nextTick() + expect(onVnodeBeforeUpdate).toBeCalled() + expect(onVnodeUpdated).toBeCalled() + + render(null, root) + await nextTick() + expect(onVnodeBeforeUnmount).toBeCalled() + expect(onVnodeUnmounted).toBeCalled() + }) + + test('component', async () => { + const Comp = () => h('div') + const onVnodeBeforeMount = jest.fn() + const onVnodeMounted = jest.fn() + const onVnodeBeforeUpdate = jest.fn() + const onVnodeUpdated = jest.fn() + const onVnodeBeforeUnmount = jest.fn() + const onVnodeUnmounted = jest.fn() + const root = nodeOps.createElement('div') + const count = ref(0) + + const App = () => + h(Comp, { + onVnodeBeforeMount, + onVnodeMounted, + onVnodeBeforeUpdate, + onVnodeUpdated, + onVnodeBeforeUnmount, + onVnodeUnmounted, + count: count.value + }) + + render(h(App), root) + await nextTick() + expect(onVnodeBeforeMount).toBeCalled() + expect(onVnodeMounted).toBeCalled() + + count.value++ + await nextTick() + expect(onVnodeBeforeUpdate).toBeCalled() + expect(onVnodeUpdated).toBeCalled() + + render(null, root) + await nextTick() + expect(onVnodeBeforeUnmount).toBeCalled() + expect(onVnodeUnmounted).toBeCalled() + }) +}) diff --git a/packages/runtime-core/src/components/Suspense.ts b/packages/runtime-core/src/components/Suspense.ts index d075ce12d32..3e15ac83d04 100644 --- a/packages/runtime-core/src/components/Suspense.ts +++ b/packages/runtime-core/src/components/Suspense.ts @@ -220,6 +220,7 @@ export interface SuspenseBoundary< setupRenderEffect: ( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null, + parentComponent: ComponentInternalInstance | null, initialVNode: VNode, container: HostElement, anchor: HostNode | null, @@ -420,6 +421,7 @@ function createSuspenseBoundary( setupRenderEffect( instance, suspense, + parentComponent, vnode, // component may have been moved before resolve parentNode(instance.subTree.el)!, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index f4c68387f63..baf893aee19 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -970,6 +970,7 @@ export function createRenderer< setupRenderEffect( instance, parentSuspense, + parentComponent, initialVNode, container, anchor, @@ -984,6 +985,7 @@ export function createRenderer< function setupRenderEffect( instance: ComponentInternalInstance, parentSuspense: HostSuspenseBoundary | null, + parentComponent: ComponentInternalInstance | null, initialVNode: HostVNode, container: HostElement, anchor: HostNode | null, @@ -993,16 +995,29 @@ export function createRenderer< instance.update = effect(function componentEffect() { if (!instance.isMounted) { const subTree = (instance.subTree = renderComponentRoot(instance)) + const { props } = instance.vnode // beforeMount hook if (instance.bm !== null) { invokeHooks(instance.bm) } + if (props && props.onVnodeBeforeMount != null) { + invokeDirectiveHook( + props.onVnodeBeforeMount, + parentComponent, + subTree + ) + } patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) initialVNode.el = subTree.el // mounted hook if (instance.m !== null) { queuePostRenderEffect(instance.m, parentSuspense) } + if (props && props.onVnodeMounted != null) { + queuePostRenderEffect(() => { + invokeDirectiveHook(props.onVnodeMounted!, parentComponent, subTree) + }, parentSuspense) + } // activated hook for keep-alive roots. if ( instance.a !== null && @@ -1016,6 +1031,7 @@ export function createRenderer< // This is triggered by mutation of component's own state (next: null) // OR parent calling processComponent (next: HostVNode) const { next } = instance + const { props } = instance.vnode if (__DEV__) { pushWarningContext(next || instance.vnode) @@ -1031,6 +1047,14 @@ export function createRenderer< if (instance.bu !== null) { invokeHooks(instance.bu) } + if (props && props.onVnodeBeforeUpdate != null) { + invokeDirectiveHook( + props.onVnodeBeforeUpdate, + parentComponent, + nextTree, + prevTree + ) + } // reset refs // only needed if previous patch had refs if (instance.refs !== EMPTY_OBJ) { @@ -1058,6 +1082,16 @@ export function createRenderer< if (instance.u !== null) { queuePostRenderEffect(instance.u, parentSuspense) } + if (props && props.onVnodeUpdated != null) { + queuePostRenderEffect(() => { + invokeDirectiveHook( + props.onVnodeUpdated!, + parentComponent, + nextTree, + prevTree + ) + }, parentSuspense) + } if (__DEV__) { popWarningContext() @@ -1539,7 +1573,12 @@ export function createRenderer< if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { ;(parentComponent!.sink as KeepAliveSink).deactivate(vnode) } else { - unmountComponent(vnode.component!, parentSuspense, doRemove) + unmountComponent( + vnode.component!, + parentSuspense, + parentComponent, + doRemove + ) } return } @@ -1621,17 +1660,31 @@ export function createRenderer< function unmountComponent( instance: ComponentInternalInstance, parentSuspense: HostSuspenseBoundary | null, + parentComponent: ComponentInternalInstance | null, doRemove?: boolean ) { if (__HMR__ && instance.type.__hmrId != null) { unregisterHMR(instance) } - const { bum, effects, update, subTree, um, da, isDeactivated } = instance + const { + bum, + effects, + update, + subTree, + um, + da, + isDeactivated, + vnode + } = instance + const { props } = vnode // beforeUnmount hook if (bum !== null) { invokeHooks(bum) } + if (props && props.onVnodeBeforeUnmount != null) { + invokeDirectiveHook(props.onVnodeBeforeUnmount, parentComponent, subTree) + } if (effects !== null) { for (let i = 0; i < effects.length; i++) { stop(effects[i]) @@ -1647,6 +1700,11 @@ export function createRenderer< if (um !== null) { queuePostRenderEffect(um, parentSuspense) } + if (props && props.onVnodeUnmounted != null) { + queuePostRenderEffect(() => { + invokeDirectiveHook(props.onVnodeUnmounted!, parentComponent, subTree) + }, parentSuspense) + } // deactivated hook if ( da !== null &&