diff --git a/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts b/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts index dc31059f2..72d62c593 100644 --- a/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts +++ b/packages/runtime-vapor/__tests__/apiLifecycle.spec.ts @@ -136,7 +136,7 @@ describe('api: lifecycle hooks', () => { it('onBeforeUnmount', async () => { const toggle = ref(true) const fn = vi.fn(() => { - expect(host.innerHTML).toBe('
') + expect(host.innerHTML).toBe('') }) const { render, host } = define({ setup() { @@ -165,14 +165,14 @@ describe('api: lifecycle hooks', () => { toggle.value = false await nextTick() - // expect(fn).toHaveBeenCalledTimes(1) // FIXME: not called + expect(fn).toHaveBeenCalledTimes(1) expect(host.innerHTML).toBe('') }) it('onUnmounted', async () => { const toggle = ref(true) const fn = vi.fn(() => { - expect(host.innerHTML).toBe('') + expect(host.innerHTML).toBe('') }) const { render, host } = define({ setup() { @@ -201,14 +201,14 @@ describe('api: lifecycle hooks', () => { toggle.value = false await nextTick() - // expect(fn).toHaveBeenCalledTimes(1) // FIXME: not called + expect(fn).toHaveBeenCalledTimes(1) expect(host.innerHTML).toBe('') }) it('onBeforeUnmount in onMounted', async () => { const toggle = ref(true) const fn = vi.fn(() => { - expect(host.innerHTML).toBe('') + expect(host.innerHTML).toBe('') }) const { render, host } = define({ setup() { @@ -239,29 +239,32 @@ describe('api: lifecycle hooks', () => { toggle.value = false await nextTick() - // expect(fn).toHaveBeenCalledTimes(1) // FIXME: not called + expect(fn).toHaveBeenCalledTimes(1) expect(host.innerHTML).toBe('') }) it('lifecycle call order', async () => { - const count = ref(0) + const rootCounter = ref(0) + const propsCounter = ref(0) const toggle = ref(true) const calls: string[] = [] const { render } = define({ setup() { - onBeforeMount(() => calls.push('onBeforeMount')) - onMounted(() => calls.push('onMounted')) - onBeforeUpdate(() => calls.push('onBeforeUpdate')) - onUpdated(() => calls.push('onUpdated')) - onBeforeUnmount(() => calls.push('onBeforeUnmount')) - onUnmounted(() => calls.push('onUnmounted')) + onBeforeMount(() => calls.push('root onBeforeMount')) + onMounted(() => calls.push('root onMounted')) + onBeforeUpdate(() => calls.push('root onBeforeUpdate')) + onUpdated(() => calls.push('root onUpdated')) + onBeforeUnmount(() => calls.push('root onBeforeUnmount')) + onUnmounted(() => calls.push('root onUnmounted')) return (() => { - const n0 = createIf( + const n0 = template('')() + renderEffect(() => setText(n0, rootCounter.value)) + const n1 = createIf( () => toggle.value, - () => createComponent(Mid, { count: () => count.value }), + () => createComponent(Mid, { count: () => propsCounter.value }), ) - return n0 + return [n0, n1] })() }, }) @@ -303,42 +306,65 @@ describe('api: lifecycle hooks', () => { // mount render() expect(calls).toEqual([ - 'onBeforeMount', + 'root onBeforeMount', 'mid onBeforeMount', 'child onBeforeMount', 'child onMounted', 'mid onMounted', - 'onMounted', + 'root onMounted', ]) calls.length = 0 - // update - count.value++ + // props update + propsCounter.value++ + await nextTick() + // There are no calls in the root and mid, + // but maybe such performance would be better. + expect(calls).toEqual([ + // 'root onBeforeUpdate', + // 'mid onBeforeUpdate', + 'child onBeforeUpdate', + 'child onUpdated', + // 'mid onUpdated', + // 'root onUpdated', + ]) + + calls.length = 0 + + // root update + rootCounter.value++ await nextTick() - // FIXME: not called - // expect(calls).toEqual([ - // 'root onBeforeUpdate', - // 'mid onBeforeUpdate', - // 'child onBeforeUpdate', - // 'child onUpdated', - // 'mid onUpdated', - // 'root onUpdated', - // ]) + // Root update events should not be passed to children. + expect(calls).toEqual(['root onBeforeUpdate', 'root onUpdated']) calls.length = 0 // unmount toggle.value = false - // FIXME: not called - // expect(calls).toEqual([ - // 'root onBeforeUnmount', - // 'mid onBeforeUnmount', - // 'child onBeforeUnmount', - // 'child onUnmounted', - // 'mid onUnmounted', - // 'root onUnmounted', - // ]) + await nextTick() + expect(calls).toEqual([ + 'root onBeforeUpdate', + 'mid onBeforeUnmount', + 'child onBeforeUnmount', + 'child onUnmounted', + 'mid onUnmounted', + 'root onUpdated', + ]) + + calls.length = 0 + + // mount + toggle.value = true + await nextTick() + expect(calls).toEqual([ + 'root onBeforeUpdate', + 'mid onBeforeMount', + 'child onBeforeMount', + 'child onMounted', + 'mid onMounted', + 'root onUpdated', + ]) }) it('onRenderTracked', async () => { @@ -458,7 +484,7 @@ describe('api: lifecycle hooks', () => { expect(fn).toHaveBeenCalledTimes(2) toggle.value = false await nextTick() - // expect(fn).toHaveBeenCalledTimes(4) // FIXME: not called unmounted hook + expect(fn).toHaveBeenCalledTimes(4) }) // #136 diff --git a/packages/runtime-vapor/__tests__/component.spec.ts b/packages/runtime-vapor/__tests__/component.spec.ts index c166a8c73..64cbebe8e 100644 --- a/packages/runtime-vapor/__tests__/component.spec.ts +++ b/packages/runtime-vapor/__tests__/component.spec.ts @@ -1,4 +1,15 @@ -import { ref, setText, template, watchEffect } from '../src' +import { + type Directive, + createComponent, + createIf, + nextTick, + ref, + renderEffect, + setText, + template, + watchEffect, + withDirectives, +} from '../src' import { describe, expect } from 'vitest' import { makeRender } from './_utils' @@ -19,4 +30,129 @@ describe('component', () => { app.unmount() expect(host.innerHTML).toBe('') }) + + it('directive lifecycle hooks call order', async () => { + const rootCounter = ref(0) + const propsCounter = ref(0) + const toggle = ref(true) + const calls: string[] = [] + + const vDirective = (name: string): Directive => ({ + created: () => calls.push(`${name} created`), + beforeMount: () => calls.push(`${name} beforeMount`), + mounted: () => calls.push(`${name} mounted`), + beforeUpdate: () => calls.push(`${name} beforeUpdate`), + updated: () => calls.push(`${name} updated`), + beforeUnmount: () => calls.push(`${name} beforeUnmount`), + unmounted: () => calls.push(`${name} unmounted`), + }) + + const { render } = define({ + setup() { + return (() => { + const n0 = withDirectives(template('')(), [ + [vDirective('root')], + ]) + renderEffect(() => setText(n0, rootCounter.value)) + const n1 = createIf( + () => toggle.value, + () => createComponent(Mid, { count: () => propsCounter.value }), + ) + return [n0, n1] + })() + }, + }) + + const Mid = { + props: ['count'], + setup(props: any) { + return (() => { + withDirectives(template('')(), [[vDirective('mid')]]) + const n0 = createComponent(Child, { count: () => props.count }) + return n0 + })() + }, + } + + const Child = { + props: ['count'], + setup(props: any) { + return (() => { + const t0 = template('') + const n0 = t0() + withDirectives(n0, [[vDirective('child')]]) + renderEffect(() => setText(n0, props.count)) + return n0 + })() + }, + } + + // mount + render() + expect(calls).toEqual([ + 'root created', + 'mid created', + 'child created', + 'root beforeMount', + 'mid beforeMount', + 'child beforeMount', + 'root mounted', + 'mid mounted', + 'child mounted', + ]) + + calls.length = 0 + + // props update + propsCounter.value++ + await nextTick() + // There are no calls in the root and mid, + // but maybe such performance would be better. + expect(calls).toEqual([ + // 'root beforeUpdate', + // 'mid beforeUpdate', + 'child beforeUpdate', + 'child updated', + // 'mid updated', + // 'root updated', + ]) + + calls.length = 0 + + // root update + rootCounter.value++ + await nextTick() + // Root update events should not be passed to children. + expect(calls).toEqual(['root beforeUpdate', 'root updated']) + + calls.length = 0 + + // unmount + toggle.value = false + await nextTick() + expect(calls).toEqual([ + 'root beforeUpdate', + 'mid beforeUnmount', + 'child beforeUnmount', + 'mid unmounted', + 'child unmounted', + 'root updated', + ]) + + calls.length = 0 + + // mount + toggle.value = true + await nextTick() + expect(calls).toEqual([ + 'root beforeUpdate', + 'mid created', + 'child created', + 'mid beforeMount', + 'child beforeMount', + 'mid mounted', + 'child mounted', + 'root updated', + ]) + }) }) diff --git a/packages/runtime-vapor/src/apiCreateComponent.ts b/packages/runtime-vapor/src/apiCreateComponent.ts index 15a37133f..622024f85 100644 --- a/packages/runtime-vapor/src/apiCreateComponent.ts +++ b/packages/runtime-vapor/src/apiCreateComponent.ts @@ -1,12 +1,13 @@ -import { - type Component, - createComponentInstance, - currentInstance, -} from './component' +import { type Component, createComponentInstance } from './component' import { setupComponent } from './apiRender' import type { RawProps } from './componentProps' import type { RawSlots } from './componentSlots' import { withAttrs } from './componentAttrs' +import { getCurrentScope } from '@vue/reactivity' +import type { BlockEffectScope } from './blockEffectScope' +import { setDirectiveBinding } from './directives' +import { VaporLifecycleHooks } from './enums' +import { scheduleLifecycleHooks } from './componentLifecycle' export function createComponent( comp: Component, @@ -15,7 +16,7 @@ export function createComponent( singleRoot: boolean = false, once: boolean = false, ) { - const current = currentInstance! + const parentScope = getCurrentScope() as BlockEffectScope const instance = createComponentInstance( comp, singleRoot ? withAttrs(rawProps) : rawProps, @@ -24,8 +25,39 @@ export function createComponent( ) setupComponent(instance, singleRoot) - // register sub-component with current component for lifecycle management - current.comps.add(instance) + setDirectiveBinding( + instance, + instance, + { + beforeMount: scheduleLifecycleHooks( + instance, + VaporLifecycleHooks.BEFORE_MOUNT, + 'beforeMount', + ), + mounted: scheduleLifecycleHooks( + instance, + VaporLifecycleHooks.MOUNTED, + 'mounted', + () => (instance.isMounted = true), + true, + ), + beforeUnmount: scheduleLifecycleHooks( + instance, + VaporLifecycleHooks.BEFORE_UNMOUNT, + 'beforeUnmount', + ), + unmounted: scheduleLifecycleHooks( + instance, + VaporLifecycleHooks.UNMOUNTED, + 'unmounted', + () => (instance.isUnmounted = true), + true, + ), + }, + null, + undefined, + parentScope, + ) return instance } diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 07bbd151f..f405c7593 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -18,7 +18,7 @@ import { type Block, type Fragment, fragmentKey } from './apiRender' import { warn } from './warning' import { currentInstance } from './component' import { componentKey } from './component' -import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope' +import { BlockEffectScope, isBlockEffectScope } from './blockEffectScope' import { createChildFragmentDirectives, invokeWithMount, @@ -62,7 +62,7 @@ export const createFor = ( } const instance = currentInstance! - if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) { + if (__DEV__ && (!instance || !isBlockEffectScope(parentScope))) { warn('createFor() can only be used inside setup()') } diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index dce38bae9..1034f46a1 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -3,7 +3,7 @@ import { getCurrentScope } from '@vue/reactivity' import { createComment, createTextNode, insert, remove } from './dom/element' import { currentInstance } from './component' import { warn } from './warning' -import { BlockEffectScope, isRenderEffectScope } from './blockEffectScope' +import { BlockEffectScope, isBlockEffectScope } from './blockEffectScope' import { createChildFragmentDirectives, invokeWithMount, @@ -36,7 +36,7 @@ export const createIf = ( } const instance = currentInstance! - if (__DEV__ && (!instance || !isRenderEffectScope(parentScope))) { + if (__DEV__ && (!instance || !isBlockEffectScope(parentScope))) { warn('createIf() can only be used inside setup()') } diff --git a/packages/runtime-vapor/src/apiLifecycle.ts b/packages/runtime-vapor/src/apiLifecycle.ts index d3c69b58f..1ea749f6d 100644 --- a/packages/runtime-vapor/src/apiLifecycle.ts +++ b/packages/runtime-vapor/src/apiLifecycle.ts @@ -24,9 +24,6 @@ const injectHook = ( const wrappedHook = hook.__weh || (hook.__weh = (...args: unknown[]) => { - if (target.isUnmounted) { - return - } pauseTracking() const reset = setCurrentInstance(target) const res = target.scope.run(() => diff --git a/packages/runtime-vapor/src/apiRender.ts b/packages/runtime-vapor/src/apiRender.ts index 086f18d63..252d16fca 100644 --- a/packages/runtime-vapor/src/apiRender.ts +++ b/packages/runtime-vapor/src/apiRender.ts @@ -6,7 +6,7 @@ import { validateComponentName, } from './component' import { insert, querySelector, remove } from './dom/element' -import { flushPostFlushCbs, queuePostFlushCb } from './scheduler' +import { flushPostFlushCbs } from './scheduler' import { invokeLifecycle } from './componentLifecycle' import { VaporLifecycleHooks } from './enums' import { @@ -137,7 +137,7 @@ function mountComponent( instance, VaporLifecycleHooks.MOUNTED, 'mounted', - instance => (instance.isMounted = true), + () => (instance.isMounted = true), true, ) @@ -166,7 +166,7 @@ export function unmountComponent(instance: ComponentInternalInstance) { instance, VaporLifecycleHooks.UNMOUNTED, 'unmounted', - instance => queuePostFlushCb(() => (instance.isUnmounted = true)), + () => (instance.isUnmounted = true), true, ) flushPostFlushCbs() diff --git a/packages/runtime-vapor/src/blockEffectScope.ts b/packages/runtime-vapor/src/blockEffectScope.ts index 92fc01983..c7a8961d5 100644 --- a/packages/runtime-vapor/src/blockEffectScope.ts +++ b/packages/runtime-vapor/src/blockEffectScope.ts @@ -29,7 +29,7 @@ export class BlockEffectScope extends EffectScope { } } -export function isRenderEffectScope( +export function isBlockEffectScope( scope: EffectScope | undefined, ): scope is BlockEffectScope { return scope instanceof BlockEffectScope diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 4c4b02a52..256c98eae 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -162,7 +162,6 @@ export interface ComponentInternalInstance { provides: Data scope: BlockEffectScope - comps: Set