From c223eb26847debfb57d433a994b993bf5030ae1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Fri, 15 Nov 2024 03:47:35 +0800 Subject: [PATCH] fix(runtime-vapor): switch to fallback when slot is empty --- .../__tests__/componentSlots.spec.ts | 21 ++- packages/runtime-vapor/src/apiCreateIf.ts | 9 +- packages/runtime-vapor/src/componentSlots.ts | 129 +++++++++++++----- playground/src/main.ts | 2 +- 4 files changed, 119 insertions(+), 42 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 987130820..224e160e1 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -365,7 +365,7 @@ describe('component: slots', () => { describe('createSlot', () => { test('slot should be render correctly', () => { const Comp = defineComponent(() => { - const n0 = template('
')() + const n0 = template('
')() insert(createSlot('header'), n0 as any as ParentNode) return n0 }) @@ -589,7 +589,7 @@ describe('component: slots', () => { return createComponent(Comp, {}, {}) }).render() - expect(host.innerHTML).toBe('
fallback
') + expect(host.innerHTML).toBe('
fallback
') }) test('dynamic slot should be updated correctly', async () => { @@ -638,7 +638,7 @@ describe('component: slots', () => { const slotOutletName = ref('one') const Child = defineComponent(() => { - const temp0 = template('

') + const temp0 = template('

') const el0 = temp0() const slot1 = createSlot( () => slotOutletName.value, @@ -672,5 +672,20 @@ describe('component: slots', () => { expect(host.innerHTML).toBe('

fallback

') }) + + test('non-exist slot', async () => { + const Child = defineComponent(() => { + const el0 = template('

')() + 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('

') + }) }) }) diff --git a/packages/runtime-vapor/src/apiCreateIf.ts b/packages/runtime-vapor/src/apiCreateIf.ts index b3e5799e2..4356fbf29 100644 --- a/packages/runtime-vapor/src/apiCreateIf.ts +++ b/packages/runtime-vapor/src/apiCreateIf.ts @@ -1,6 +1,6 @@ import { renderEffect } from './renderEffect' 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' type BlockFn = () => Block @@ -16,15 +16,14 @@ export const createIf = ( let newValue: any let oldValue: any let branch: BlockFn | undefined - let parent: ParentNode | undefined | null let block: Block | undefined let scope: EffectScope | undefined const anchor = __DEV__ ? createComment('if') : createTextNode() - const fragment: Fragment = { + const fragment: Fragment = shallowReactive({ nodes: [], anchor, [fragmentKey]: true, - } + }) // TODO: SSR // if (isHydrating) { @@ -47,7 +46,7 @@ export const createIf = ( function doIf() { if ((newValue = !!condition()) !== oldValue) { - parent ||= anchor.parentNode + const parent = anchor.parentNode if (block) { scope!.stop() remove(block, parent!) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 87d57ec8e..0d321526f 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -4,6 +4,7 @@ import { effectScope, isReactive, shallowReactive, + shallowRef, } from '@vue/reactivity' import { type ComponentInternalInstance, @@ -12,7 +13,13 @@ import { } from './component' import { type Block, type Fragment, fragmentKey } from './apiRender' 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 { Data } from '@vue/runtime-shared' import { mergeProps } from './dom/prop' @@ -107,27 +114,30 @@ export function initSlots( export function createSlot( name: string | (() => string), binds?: NormalizedRawProps, - fallback?: () => Block, + fallback?: Slot, ): Block { - let block: Block | undefined - 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 + const { slots } = currentInstance! - // When not using dynamic slots, simplify the process to improve performance - if (!isDynamicName && !isReactive(slots)) { - if ((branch = withProps(slots[name]) || fallback)) { - return branch(binds) + const slotBlock = shallowRef() + let slotBranch: Slot | undefined + let slotScope: EffectScope | undefined + + 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 { return [] } } - const getSlot = isDynamicName ? () => slots[name()] : () => slots[name] const anchor = __DEV__ ? createComment('slot') : createTextNode() const fragment: Fragment = { nodes: [], @@ -137,29 +147,76 @@ export function createSlot( // TODO lifecycle hooks renderEffect(() => { - if ((branch = withProps(getSlot()) || fallback) !== oldBranch) { - parent ||= anchor.parentNode - if (block) { - scope!.stop() - remove(block, parent!) - } - if ((oldBranch = branch)) { - scope = effectScope() - fragment.nodes = block = scope.run(() => branch!(binds))! - parent && insert(block, parent, anchor) - } else { - scope = block = undefined - fragment.nodes = [] - } + const parent = anchor.parentNode + + if ( + !slotBlock.value || // not initied + fallbackScope || // in fallback slot + isValidBlock(slotBlock.value) // slot block is valid + ) { + renderSlot(parent) + } else { + renderFallback(parent) } }) return fragment - function withProps any>(fn?: T) { - if (fn) - return (binds?: NormalizedRawProps): ReturnType => - fn(binds && normalizeSlotProps(binds)) + function renderSlot(parent: ParentNode | null) { + // from fallback to slot + const fromFallback = fallbackScope + 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 + ) +} diff --git a/playground/src/main.ts b/playground/src/main.ts index c65d9c2ec..d2999613d 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -4,7 +4,7 @@ import { createVaporApp } from 'vue/vapor' import { createApp } from 'vue' import './style.css' -const modules = import.meta.glob('./**/*.(vue|js)') +const modules = import.meta.glob('./**/*.(vue|js|ts)') const mod = (modules['.' + location.pathname] || modules['./App.vue'])() mod.then(({ default: mod }) => {