fix(runtime-vapor): switch to fallback when slot is empty

This commit is contained in:
三咲智子 Kevin Deng 2024-11-15 03:47:35 +08:00
parent 7f3ca46523
commit c223eb2684
No known key found for this signature in database
4 changed files with 119 additions and 42 deletions

View File

@ -365,7 +365,7 @@ describe('component: slots', () => {
describe('createSlot', () => { describe('createSlot', () => {
test('slot should be render correctly', () => { test('slot should be render correctly', () => {
const Comp = defineComponent(() => { const Comp = defineComponent(() => {
const n0 = template('<div></div>')() const n0 = template('<div>')()
insert(createSlot('header'), n0 as any as ParentNode) insert(createSlot('header'), n0 as any as ParentNode)
return n0 return n0
}) })
@ -589,7 +589,7 @@ describe('component: slots', () => {
return createComponent(Comp, {}, {}) return createComponent(Comp, {}, {})
}).render() }).render()
expect(host.innerHTML).toBe('<div>fallback</div>') expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
}) })
test('dynamic slot should be updated correctly', async () => { test('dynamic slot should be updated correctly', async () => {
@ -638,7 +638,7 @@ describe('component: slots', () => {
const slotOutletName = ref('one') const slotOutletName = ref('one')
const Child = defineComponent(() => { const Child = defineComponent(() => {
const temp0 = template('<p></p>') const temp0 = template('<p>')
const el0 = temp0() const el0 = temp0()
const slot1 = createSlot( const slot1 = createSlot(
() => slotOutletName.value, () => slotOutletName.value,
@ -672,5 +672,20 @@ describe('component: slots', () => {
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>') expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
}) })
test('non-exist slot', async () => {
const Child = defineComponent(() => {
const el0 = template('<p>')()
const slot = createSlot('not-exist', undefined)
insert(slot, el0 as any as ParentNode)
return el0
})
const { host } = define(() => {
return createComponent(Child)
}).render()
expect(host.innerHTML).toBe('<p></p>')
})
}) })
}) })

View File

@ -1,6 +1,6 @@
import { renderEffect } from './renderEffect' import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender' import { type Block, type Fragment, fragmentKey } from './apiRender'
import { type EffectScope, effectScope } from '@vue/reactivity' import { type EffectScope, effectScope, shallowReactive } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom/element' import { createComment, createTextNode, insert, remove } from './dom/element'
type BlockFn = () => Block type BlockFn = () => Block
@ -16,15 +16,14 @@ export const createIf = (
let newValue: any let newValue: any
let oldValue: any let oldValue: any
let branch: BlockFn | undefined let branch: BlockFn | undefined
let parent: ParentNode | undefined | null
let block: Block | undefined let block: Block | undefined
let scope: EffectScope | undefined let scope: EffectScope | undefined
const anchor = __DEV__ ? createComment('if') : createTextNode() const anchor = __DEV__ ? createComment('if') : createTextNode()
const fragment: Fragment = { const fragment: Fragment = shallowReactive({
nodes: [], nodes: [],
anchor, anchor,
[fragmentKey]: true, [fragmentKey]: true,
} })
// TODO: SSR // TODO: SSR
// if (isHydrating) { // if (isHydrating) {
@ -47,7 +46,7 @@ export const createIf = (
function doIf() { function doIf() {
if ((newValue = !!condition()) !== oldValue) { if ((newValue = !!condition()) !== oldValue) {
parent ||= anchor.parentNode const parent = anchor.parentNode
if (block) { if (block) {
scope!.stop() scope!.stop()
remove(block, parent!) remove(block, parent!)

View File

@ -4,6 +4,7 @@ import {
effectScope, effectScope,
isReactive, isReactive,
shallowReactive, shallowReactive,
shallowRef,
} from '@vue/reactivity' } from '@vue/reactivity'
import { import {
type ComponentInternalInstance, type ComponentInternalInstance,
@ -12,7 +13,13 @@ import {
} from './component' } from './component'
import { type Block, type Fragment, fragmentKey } from './apiRender' import { type Block, type Fragment, fragmentKey } from './apiRender'
import { firstEffect, renderEffect } from './renderEffect' import { firstEffect, renderEffect } from './renderEffect'
import { createComment, createTextNode, insert, remove } from './dom/element' import {
createComment,
createTextNode,
insert,
normalizeBlock,
remove,
} from './dom/element'
import type { NormalizedRawProps } from './componentProps' import type { NormalizedRawProps } from './componentProps'
import type { Data } from '@vue/runtime-shared' import type { Data } from '@vue/runtime-shared'
import { mergeProps } from './dom/prop' import { mergeProps } from './dom/prop'
@ -107,27 +114,30 @@ export function initSlots(
export function createSlot( export function createSlot(
name: string | (() => string), name: string | (() => string),
binds?: NormalizedRawProps, binds?: NormalizedRawProps,
fallback?: () => Block, fallback?: Slot,
): Block { ): Block {
let block: Block | undefined const { slots } = currentInstance!
let branch: Slot | undefined
let oldBranch: Slot | undefined
let parent: ParentNode | undefined | null
let scope: EffectScope | undefined
const isDynamicName = isFunction(name)
const instance = currentInstance!
const { slots } = instance
// When not using dynamic slots, simplify the process to improve performance const slotBlock = shallowRef<Block>()
if (!isDynamicName && !isReactive(slots)) { let slotBranch: Slot | undefined
if ((branch = withProps(slots[name]) || fallback)) { let slotScope: EffectScope | undefined
return branch(binds)
let fallbackBlock: Block | undefined
let fallbackBranch: Slot | undefined
let fallbackScope: EffectScope | undefined
const normalizeBinds = binds && normalizeSlotProps(binds)
const isDynamicName = isFunction(name)
// fast path for static slots & without fallback
if (!isDynamicName && !isReactive(slots) && !fallback) {
if ((slotBranch = slots[name])) {
return slotBranch(normalizeBinds)
} else { } else {
return [] return []
} }
} }
const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
const anchor = __DEV__ ? createComment('slot') : createTextNode() const anchor = __DEV__ ? createComment('slot') : createTextNode()
const fragment: Fragment = { const fragment: Fragment = {
nodes: [], nodes: [],
@ -137,29 +147,76 @@ export function createSlot(
// TODO lifecycle hooks // TODO lifecycle hooks
renderEffect(() => { renderEffect(() => {
if ((branch = withProps(getSlot()) || fallback) !== oldBranch) { const parent = anchor.parentNode
parent ||= anchor.parentNode
if (block) { if (
scope!.stop() !slotBlock.value || // not initied
remove(block, parent!) fallbackScope || // in fallback slot
} isValidBlock(slotBlock.value) // slot block is valid
if ((oldBranch = branch)) { ) {
scope = effectScope() renderSlot(parent)
fragment.nodes = block = scope.run(() => branch!(binds))!
parent && insert(block, parent, anchor)
} else { } else {
scope = block = undefined renderFallback(parent)
fragment.nodes = []
}
} }
}) })
return fragment return fragment
function withProps<T extends (p: any) => any>(fn?: T) { function renderSlot(parent: ParentNode | null) {
if (fn) // from fallback to slot
return (binds?: NormalizedRawProps): ReturnType<T> => const fromFallback = fallbackScope
fn(binds && normalizeSlotProps(binds)) if (fromFallback) {
// clean fallback slot
fallbackScope!.stop()
remove(fallbackBlock!, parent!)
fallbackScope = fallbackBlock = undefined
}
const slotName = isFunction(name) ? name() : name
const branch = slots[slotName]!
if (branch) {
// init slot scope and block or switch branch
if (!slotScope || slotBranch !== branch) {
// clean previous slot
if (slotScope && !fromFallback) {
slotScope.stop()
remove(slotBlock.value!, parent!)
}
slotBranch = branch
slotScope = effectScope()
slotBlock.value = slotScope.run(() => slotBranch!(normalizeBinds))
}
// if slot block is valid, render it
if (slotBlock.value && isValidBlock(slotBlock.value)) {
fragment.nodes = slotBlock.value
parent && insert(slotBlock.value, parent, anchor)
} else {
renderFallback(parent)
}
} else {
renderFallback(parent)
}
}
function renderFallback(parent: ParentNode | null) {
// if slot branch is initied, remove it from DOM, but keep the scope
if (slotBranch) {
remove(slotBlock.value!, parent!)
}
fallbackBranch ||= fallback
if (fallbackBranch) {
fallbackScope = effectScope()
fragment.nodes = fallbackBlock = fallbackScope.run(() =>
fallbackBranch!(normalizeBinds),
)!
parent && insert(fallbackBlock, parent, anchor)
} else {
fragment.nodes = []
}
} }
} }
@ -214,3 +271,9 @@ function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
} }
} }
} }
function isValidBlock(block: Block) {
return (
normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0
)
}

View File

@ -4,7 +4,7 @@ import { createVaporApp } from 'vue/vapor'
import { createApp } from 'vue' import { createApp } from 'vue'
import './style.css' import './style.css'
const modules = import.meta.glob<any>('./**/*.(vue|js)') const modules = import.meta.glob<any>('./**/*.(vue|js|ts)')
const mod = (modules['.' + location.pathname] || modules['./App.vue'])() const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
mod.then(({ default: mod }) => { mod.then(({ default: mod }) => {