Skip to content

Commit 92c2d8c

Browse files
authored
fix(runtime-vapor): use computed to cache the result of dynamic slot function to avoid redundant calls. (#14176)
1 parent e499d56 commit 92c2d8c

File tree

2 files changed

+199
-38
lines changed

2 files changed

+199
-38
lines changed

packages/runtime-vapor/__tests__/componentSlots.spec.ts

Lines changed: 180 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -103,41 +103,6 @@ describe('component: slots', () => {
103103
expect(instance.slots).toHaveProperty('two')
104104
})
105105

106-
test('should work with createFlorSlots', async () => {
107-
const loop = ref([1, 2, 3])
108-
109-
let instance: any
110-
const Child = () => {
111-
instance = currentInstance
112-
return template('child')()
113-
}
114-
115-
const { render } = define({
116-
setup() {
117-
return createComponent(Child, null, {
118-
$: [
119-
() =>
120-
createForSlots(loop.value, (item, i) => ({
121-
name: item,
122-
fn: () => template(item + i)(),
123-
})),
124-
],
125-
})
126-
},
127-
})
128-
render()
129-
130-
expect(instance.slots).toHaveProperty('1')
131-
expect(instance.slots).toHaveProperty('2')
132-
expect(instance.slots).toHaveProperty('3')
133-
loop.value.push(4)
134-
await nextTick()
135-
expect(instance.slots).toHaveProperty('4')
136-
loop.value.shift()
137-
await nextTick()
138-
expect(instance.slots).not.toHaveProperty('1')
139-
})
140-
141106
// passes but no warning for slot invocation in vapor currently
142107
test.todo('should not warn when mounting another app in setup', () => {
143108
const Comp = defineVaporComponent({
@@ -1936,4 +1901,184 @@ describe('component: slots', () => {
19361901
})
19371902
})
19381903
})
1904+
1905+
describe('createForSlots', () => {
1906+
test('should work', async () => {
1907+
const loop = ref([1, 2, 3])
1908+
1909+
let instance: any
1910+
const Child = () => {
1911+
instance = currentInstance
1912+
return template('child')()
1913+
}
1914+
1915+
const { render } = define({
1916+
setup() {
1917+
return createComponent(Child, null, {
1918+
$: [
1919+
() =>
1920+
createForSlots(loop.value, (item, i) => ({
1921+
name: item,
1922+
fn: () => template(item + i)(),
1923+
})),
1924+
],
1925+
})
1926+
},
1927+
})
1928+
render()
1929+
1930+
expect(instance.slots).toHaveProperty('1')
1931+
expect(instance.slots).toHaveProperty('2')
1932+
expect(instance.slots).toHaveProperty('3')
1933+
loop.value.push(4)
1934+
await nextTick()
1935+
expect(instance.slots).toHaveProperty('4')
1936+
loop.value.shift()
1937+
await nextTick()
1938+
expect(instance.slots).not.toHaveProperty('1')
1939+
})
1940+
1941+
test('should cache dynamic slot source result', async () => {
1942+
const items = ref([1, 2, 3])
1943+
let callCount = 0
1944+
1945+
const getItems = () => {
1946+
callCount++
1947+
return items.value
1948+
}
1949+
1950+
let instance: any
1951+
const Child = defineVaporComponent(() => {
1952+
instance = currentInstance
1953+
// Create multiple slots to trigger multiple getSlot calls
1954+
const n1 = template('<div></div>')()
1955+
const n2 = template('<div></div>')()
1956+
const n3 = template('<div></div>')()
1957+
insert(createSlot('slot1'), n1 as any as ParentNode)
1958+
insert(createSlot('slot2'), n2 as any as ParentNode)
1959+
insert(createSlot('slot3'), n3 as any as ParentNode)
1960+
return [n1, n2, n3]
1961+
})
1962+
1963+
define({
1964+
setup() {
1965+
return createComponent(Child, null, {
1966+
$: [
1967+
() =>
1968+
createForSlots(getItems(), (item, i) => ({
1969+
name: 'slot' + item,
1970+
fn: () => template(String(item))(),
1971+
})),
1972+
],
1973+
})
1974+
},
1975+
}).render()
1976+
1977+
// getItems should only be called once
1978+
expect(callCount).toBe(1)
1979+
1980+
expect(instance.slots).toHaveProperty('slot1')
1981+
expect(instance.slots).toHaveProperty('slot2')
1982+
expect(instance.slots).toHaveProperty('slot3')
1983+
})
1984+
1985+
test('should update when source changes', async () => {
1986+
const items = ref([1, 2])
1987+
let callCount = 0
1988+
1989+
const getItems = () => {
1990+
callCount++
1991+
return items.value
1992+
}
1993+
1994+
let instance: any
1995+
const Child = defineVaporComponent(() => {
1996+
instance = currentInstance
1997+
const n1 = template('<div></div>')()
1998+
const n2 = template('<div></div>')()
1999+
const n3 = template('<div></div>')()
2000+
insert(createSlot('slot1'), n1 as any as ParentNode)
2001+
insert(createSlot('slot2'), n2 as any as ParentNode)
2002+
insert(createSlot('slot3'), n3 as any as ParentNode)
2003+
return [n1, n2, n3]
2004+
})
2005+
2006+
define({
2007+
setup() {
2008+
return createComponent(Child, null, {
2009+
$: [
2010+
() =>
2011+
createForSlots(getItems(), (item, i) => ({
2012+
name: 'slot' + item,
2013+
fn: () => template(String(item))(),
2014+
})),
2015+
],
2016+
})
2017+
},
2018+
}).render()
2019+
2020+
expect(callCount).toBe(1)
2021+
expect(instance.slots).toHaveProperty('slot1')
2022+
expect(instance.slots).toHaveProperty('slot2')
2023+
expect(instance.slots).not.toHaveProperty('slot3')
2024+
2025+
// Update items
2026+
items.value.push(3)
2027+
await nextTick()
2028+
2029+
// Should be called again after source changes
2030+
expect(callCount).toBe(2)
2031+
expect(instance.slots).toHaveProperty('slot1')
2032+
expect(instance.slots).toHaveProperty('slot2')
2033+
expect(instance.slots).toHaveProperty('slot3')
2034+
})
2035+
2036+
test('should render slots correctly with caching', async () => {
2037+
const items = ref([1, 2, 3, 4, 5])
2038+
2039+
const Child = defineVaporComponent(() => {
2040+
const containers: any[] = []
2041+
for (let i = 1; i <= 5; i++) {
2042+
const n = template('<div></div>')()
2043+
insert(createSlot('slot' + i), n as any as ParentNode)
2044+
containers.push(n)
2045+
}
2046+
return containers
2047+
})
2048+
2049+
const { host } = define({
2050+
setup() {
2051+
return createComponent(Child, null, {
2052+
$: [
2053+
() =>
2054+
createForSlots(items.value, item => ({
2055+
name: 'slot' + item,
2056+
fn: () => template('content' + item)(),
2057+
})),
2058+
],
2059+
})
2060+
},
2061+
}).render()
2062+
2063+
expect(host.innerHTML).toBe(
2064+
'<div>content1<!--slot--></div>' +
2065+
'<div>content2<!--slot--></div>' +
2066+
'<div>content3<!--slot--></div>' +
2067+
'<div>content4<!--slot--></div>' +
2068+
'<div>content5<!--slot--></div>',
2069+
)
2070+
2071+
// Update items
2072+
items.value = [2, 4]
2073+
await nextTick()
2074+
2075+
expect(host.innerHTML).toBe(
2076+
'<div><!--slot--></div>' +
2077+
'<div>content2<!--slot--></div>' +
2078+
'<div><!--slot--></div>' +
2079+
'<div>content4<!--slot--></div>' +
2080+
'<div><!--slot--></div>',
2081+
)
2082+
})
2083+
})
19392084
})

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared'
2+
import { type ComputedRef, computed } from '@vue/reactivity'
23
import { type Block, type BlockFn, insert, setScopeId } from './block'
34
import { rawPropsProxyHandlers } from './componentProps'
45
import {
@@ -51,9 +52,24 @@ export type StaticSlots = Record<string, VaporSlot>
5152

5253
export type VaporSlot = BlockFn
5354
export type DynamicSlot = { name: string; fn: VaporSlot }
54-
export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
55+
export type DynamicSlotFn = (() => DynamicSlot | DynamicSlot[]) & {
56+
_cache?: ComputedRef<DynamicSlot | DynamicSlot[]>
57+
}
5558
export type DynamicSlotSource = StaticSlots | DynamicSlotFn
5659

60+
/**
61+
* Get cached result of a DynamicSlotFn.
62+
* Uses computed to cache the result and avoid redundant calls.
63+
*/
64+
function resolveDynamicSlot(
65+
source: DynamicSlotFn,
66+
): DynamicSlot | DynamicSlot[] {
67+
if (!source._cache) {
68+
source._cache = computed(source)
69+
}
70+
return source._cache.value
71+
}
72+
5773
export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
5874
get: getSlot,
5975
has: (target, key: string) => !!getSlot(target, key),
@@ -74,7 +90,7 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
7490
keys = keys.filter(k => k !== '$')
7591
for (const source of dynamicSources) {
7692
if (isFunction(source)) {
77-
const slot = source()
93+
const slot = resolveDynamicSlot(source)
7894
if (isArray(slot)) {
7995
for (const s of slot) keys.push(String(s.name))
8096
} else {
@@ -103,7 +119,7 @@ export function getSlot(
103119
while (i--) {
104120
source = dynamicSources[i]
105121
if (isFunction(source)) {
106-
const slot = source()
122+
const slot = resolveDynamicSlot(source)
107123
if (slot) {
108124
if (isArray(slot)) {
109125
for (const s of slot) {

0 commit comments

Comments
 (0)