wip(vapor): fix component unmount when not at block root level

This commit is contained in:
Evan You 2025-02-07 17:04:05 +08:00
parent 99d70ddd31
commit bcd2eb7fd8
No known key found for this signature in database
GPG Key ID: 00E9AB7A6704CE0A
5 changed files with 114 additions and 85 deletions

View File

@ -21,8 +21,10 @@ import {
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { import {
createComponent, createComponent,
createFor,
createIf, createIf,
createTextNode, createTextNode,
insert,
renderEffect, renderEffect,
setText, setText,
template, template,
@ -526,4 +528,90 @@ describe('api: lifecycle hooks', () => {
expect(handleUpdated).toHaveBeenCalledTimes(1) expect(handleUpdated).toHaveBeenCalledTimes(1)
expect(handleUpdatedChild).toHaveBeenCalledTimes(1) expect(handleUpdatedChild).toHaveBeenCalledTimes(1)
}) })
test('unmount hooks when nested in if block', async () => {
const toggle = ref(true)
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('<div><span></span></div><!--if-->')
})
const fn2 = vi.fn(() => {
expect(host.innerHTML).toBe('<!--if-->')
})
const { render, host } = define({
setup() {
const n0 = createIf(
() => toggle.value,
() => {
const n1 = document.createElement('div')
const n2 = createComponent(Child)
insert(n2, n1)
return n1
},
)
return n0
},
})
const Child = {
setup() {
onBeforeUnmount(fn)
onUnmounted(fn2)
const t0 = template('<span></span>')
const n0 = t0()
return n0
},
}
render()
toggle.value = false
await nextTick()
expect(fn).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
expect(host.innerHTML).toBe('<!--if-->')
})
test('unmount hooks when nested in for blocks', async () => {
const list = ref([1])
const fn = vi.fn(() => {
expect(host.innerHTML).toBe('<div><span></span></div><!--for-->')
})
const fn2 = vi.fn(() => {
expect(host.innerHTML).toBe('<!--for-->')
})
const { render, host } = define({
setup() {
const n0 = createFor(
() => list.value,
() => {
const n1 = document.createElement('div')
const n2 = createComponent(Child)
insert(n2, n1)
return n1
},
)
return n0
},
})
const Child = {
setup() {
onBeforeUnmount(fn)
onUnmounted(fn2)
const t0 = template('<span></span>')
const n0 = t0()
return n0
},
}
render()
list.value.pop()
await nextTick()
expect(fn).toHaveBeenCalledTimes(1)
expect(fn2).toHaveBeenCalledTimes(1)
expect(host.innerHTML).toBe('<!--for-->')
})
}) })

View File

@ -75,7 +75,7 @@ export const createFor = (
let newBlocks: ForBlock[] let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null let parent: ParentNode | undefined | null
const parentAnchor = __DEV__ ? createComment('for') : createTextNode() const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const ref = new VaporFragment(oldBlocks) const frag = new VaporFragment(oldBlocks)
const instance = currentInstance! const instance = currentInstance!
if (__DEV__ && !instance) { if (__DEV__ && !instance) {
@ -265,9 +265,9 @@ export const createFor = (
} }
} }
ref.nodes = [(oldBlocks = newBlocks)] frag.nodes = [(oldBlocks = newBlocks)]
if (parentAnchor) { if (parentAnchor) {
ref.nodes.push(parentAnchor) frag.nodes.push(parentAnchor)
} }
} }
@ -338,12 +338,12 @@ export const createFor = (
} }
const unmount = ({ nodes, scope }: ForBlock) => { const unmount = ({ nodes, scope }: ForBlock) => {
removeBlock(nodes, parent!)
scope && scope.stop() scope && scope.stop()
removeBlock(nodes, parent!)
} }
once ? renderList() : renderEffect(renderList) once ? renderList() : renderEffect(renderList)
return ref return frag
} }
export function createForSlots( export function createForSlots(

View File

@ -21,7 +21,7 @@ export class VaporFragment {
nodes: Block nodes: Block
anchor?: Node anchor?: Node
insert?: (parent: ParentNode, anchor: Node | null) => void insert?: (parent: ParentNode, anchor: Node | null) => void
remove?: () => void remove?: (parent?: ParentNode) => void
constructor(nodes: Block) { constructor(nodes: Block) {
this.nodes = nodes this.nodes = nodes
@ -139,16 +139,12 @@ export function prepend(parent: ParentNode, ...blocks: Block[]): void {
* during each root remove call, and update their children list by filtering * during each root remove call, and update their children list by filtering
* unmounted children * unmounted children
*/ */
export let parentsWithUnmountedChildren: Set<VaporComponentInstance> | null = // export let parentsWithUnmountedChildren: Set<VaporComponentInstance> | null =
null // null
export function remove(block: Block, parent: ParentNode): void { export function remove(block: Block, parent?: ParentNode): void {
const isRoot = !parentsWithUnmountedChildren
if (isRoot) {
parentsWithUnmountedChildren = new Set()
}
if (block instanceof Node) { if (block instanceof Node) {
parent.removeChild(block) parent && parent.removeChild(block)
} else if (isVaporComponent(block)) { } else if (isVaporComponent(block)) {
unmountComponent(block, parent) unmountComponent(block, parent)
} else if (isArray(block)) { } else if (isArray(block)) {
@ -158,7 +154,7 @@ export function remove(block: Block, parent: ParentNode): void {
} else { } else {
// fragment // fragment
if (block.remove) { if (block.remove) {
block.remove() block.remove(parent)
} else { } else {
remove(block.nodes, parent) remove(block.nodes, parent)
} }
@ -167,12 +163,6 @@ export function remove(block: Block, parent: ParentNode): void {
;(block as DynamicFragment).scope!.stop() ;(block as DynamicFragment).scope!.stop()
} }
} }
if (isRoot) {
for (const i of parentsWithUnmountedChildren!) {
i.children = i.children.filter(n => !n.isUnmounted)
}
parentsWithUnmountedChildren = null
}
} }
/** /**

View File

@ -1,5 +1,4 @@
import { import {
type ComponentInternalInstance,
type ComponentInternalOptions, type ComponentInternalOptions,
type ComponentPropsOptions, type ComponentPropsOptions,
EffectScope, EffectScope,
@ -26,29 +25,17 @@ import {
unregisterHMR, unregisterHMR,
warn, warn,
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { import { type Block, insert, isBlock, remove } from './block'
type Block,
insert,
isBlock,
parentsWithUnmountedChildren,
remove,
} from './block'
import { import {
type ShallowRef, type ShallowRef,
markRaw, markRaw,
onScopeDispose,
pauseTracking, pauseTracking,
proxyRefs, proxyRefs,
resetTracking, resetTracking,
unref, unref,
} from '@vue/reactivity' } from '@vue/reactivity'
import { import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
EMPTY_ARR,
EMPTY_OBJ,
invokeArrayFns,
isFunction,
isString,
remove as removeItem,
} from '@vue/shared'
import { import {
type DynamicPropsSource, type DynamicPropsSource,
type RawProps, type RawProps,
@ -264,6 +251,8 @@ export function createComponent(
endMeasure(instance, 'init') endMeasure(instance, 'init')
} }
onScopeDispose(() => unmountComponent(instance), true)
return instance return instance
} }
@ -300,8 +289,6 @@ export class VaporComponentInstance implements GenericComponentInstance {
type: VaporComponent type: VaporComponent
root: GenericComponentInstance | null root: GenericComponentInstance | null
parent: GenericComponentInstance | null parent: GenericComponentInstance | null
children: VaporComponentInstance[]
vdomChildren?: ComponentInternalInstance[]
appContext: GenericAppContext appContext: GenericAppContext
block: Block block: Block
@ -379,12 +366,8 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.type = comp this.type = comp
this.parent = currentInstance this.parent = currentInstance
this.root = currentInstance ? currentInstance.root : this this.root = currentInstance ? currentInstance.root : this
this.children = []
if (currentInstance) { if (currentInstance) {
if (isVaporComponent(currentInstance)) {
currentInstance.children.push(this)
}
this.appContext = currentInstance.appContext this.appContext = currentInstance.appContext
this.provides = currentInstance.provides this.provides = currentInstance.provides
this.ids = currentInstance.ids this.ids = currentInstance.ids
@ -523,40 +506,13 @@ export function unmountComponent(
instance.scope.stop() instance.scope.stop()
for (const c of instance.children) {
unmountComponent(c)
}
instance.children = EMPTY_ARR as any
if (instance.vdomChildren) {
const unmount = instance.appContext.vapor!.vdomUnmount
for (const c of instance.vdomChildren) {
unmount(c, null)
}
instance.vdomChildren = EMPTY_ARR as any
}
if (parentNode) {
// root remove: need to both remove this instance's DOM nodes
// and also remove it from the parent's children list.
remove(instance.block, parentNode)
const parentInstance = instance.parent
instance.parent = null
if (isVaporComponent(parentInstance)) {
if (parentsWithUnmountedChildren) {
// for optimize children removal
parentsWithUnmountedChildren.add(parentInstance)
} else {
removeItem(parentInstance.children, instance)
}
}
}
if (instance.um) { if (instance.um) {
queuePostFlushCb(() => invokeArrayFns(instance.um!)) queuePostFlushCb(() => invokeArrayFns(instance.um!))
} }
instance.isUnmounted = true instance.isUnmounted = true
} else if (parentNode) { }
if (parentNode) {
remove(instance.block, parentNode) remove(instance.block, parentNode)
} }
} }

View File

@ -24,7 +24,7 @@ import {
unmountComponent, unmountComponent,
} from './component' } from './component'
import { type Block, VaporFragment, insert, remove } from './block' import { type Block, VaporFragment, insert, remove } from './block'
import { extend, isFunction, remove as removeItem } from '@vue/shared' import { extend, isFunction } from '@vue/shared'
import { type RawProps, rawPropsProxyHandlers } from './componentProps' import { type RawProps, rawPropsProxyHandlers } from './componentProps'
import type { RawSlots, VaporSlot } from './componentSlots' import type { RawSlots, VaporSlot } from './componentSlots'
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
@ -116,17 +116,14 @@ function createVDOMComponent(
undefined, undefined,
false, false,
) )
;(parentInstance.vdomChildren || (parentInstance.vdomChildren = [])).push( // TODO register unmount with onScopeDispose
vnode.component!,
)
isMounted = true isMounted = true
} else { } else {
// TODO move // TODO move
} }
} }
frag.remove = () => { frag.remove = parentNode => {
internals.umt(vnode.component!, null, true) internals.umt(vnode.component!, null, !!parentNode)
removeItem(parentInstance.vdomChildren!, vnode.component)
} }
return frag return frag
@ -144,11 +141,9 @@ function renderVDOMSlot(
let isMounted = false let isMounted = false
let fallbackNodes: Block | undefined let fallbackNodes: Block | undefined
let parentNode: ParentNode
let oldVNode: VNode | null = null let oldVNode: VNode | null = null
frag.insert = (parent, anchor) => { frag.insert = (parentNode, anchor) => {
parentNode = parent
if (!isMounted) { if (!isMounted) {
renderEffect(() => { renderEffect(() => {
const vnode = renderSlot( const vnode = renderSlot(
@ -169,7 +164,7 @@ function renderVDOMSlot(
if (oldVNode) { if (oldVNode) {
internals.um(oldVNode, parentComponent as any, null, true) internals.um(oldVNode, parentComponent as any, null, true)
} }
insert((fallbackNodes = fallback(props)), parent, anchor) insert((fallbackNodes = fallback(props)), parentNode, anchor)
} }
oldVNode = null oldVNode = null
} }
@ -179,7 +174,7 @@ function renderVDOMSlot(
// TODO move // TODO move
} }
frag.remove = () => { frag.remove = parentNode => {
if (fallbackNodes) { if (fallbackNodes) {
remove(fallbackNodes, parentNode) remove(fallbackNodes, parentNode)
} else if (oldVNode) { } else if (oldVNode) {