Skip to content

Commit e4b147a

Browse files
authored
feat(runtime-vapor): add support for async components in VaporKeepAlive (#14040)
1 parent 172cb8b commit e4b147a

File tree

8 files changed

+456
-106
lines changed

8 files changed

+456
-106
lines changed

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

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { nextTick, ref } from '@vue/runtime-dom'
1+
import { nextTick, onActivated, ref } from '@vue/runtime-dom'
22
import { type VaporComponent, createComponent } from '../src/component'
33
import { defineVaporAsyncComponent } from '../src/apiDefineAsyncComponent'
44
import { makeRender } from './_utils'
55
import {
6+
VaporKeepAlive,
67
createIf,
78
createTemplateRefSetter,
9+
defineVaporComponent,
810
renderEffect,
911
template,
1012
} from '@vue/runtime-vapor'
@@ -758,7 +760,102 @@ describe('api: defineAsyncComponent', () => {
758760

759761
test.todo('suspense with error handling', async () => {})
760762

761-
test.todo('with KeepAlive', async () => {})
763+
test('with KeepAlive', async () => {
764+
const spy = vi.fn()
765+
let resolve: (comp: VaporComponent) => void
766+
767+
const Foo = defineVaporAsyncComponent(
768+
() =>
769+
new Promise(r => {
770+
resolve = r as any
771+
}),
772+
)
773+
774+
const Bar = defineVaporAsyncComponent(() =>
775+
Promise.resolve(
776+
defineVaporComponent({
777+
setup() {
778+
return template('Bar')()
779+
},
780+
}),
781+
),
782+
)
783+
784+
const toggle = ref(true)
785+
const { html } = define({
786+
setup() {
787+
return createComponent(VaporKeepAlive, null, {
788+
default: () =>
789+
createIf(
790+
() => toggle.value,
791+
() => createComponent(Foo),
792+
() => createComponent(Bar),
793+
),
794+
})
795+
},
796+
}).render()
797+
expect(html()).toBe('<!--async component--><!--if-->')
798+
799+
await nextTick()
800+
resolve!(
801+
defineVaporComponent({
802+
setup() {
803+
onActivated(() => {
804+
spy()
805+
})
806+
return template('Foo')()
807+
},
808+
}),
809+
)
810+
811+
await timeout()
812+
expect(html()).toBe('Foo<!--async component--><!--if-->')
813+
expect(spy).toBeCalledTimes(1)
762814

763-
test.todo('with KeepAlive + include', async () => {})
815+
toggle.value = false
816+
await timeout()
817+
expect(html()).toBe('Bar<!--async component--><!--if-->')
818+
})
819+
820+
test('with KeepAlive + include', async () => {
821+
const spy = vi.fn()
822+
let resolve: (comp: VaporComponent) => void
823+
824+
const Foo = defineVaporAsyncComponent(
825+
() =>
826+
new Promise(r => {
827+
resolve = r as any
828+
}),
829+
)
830+
831+
const { html } = define({
832+
setup() {
833+
return createComponent(
834+
VaporKeepAlive,
835+
{ include: () => 'Foo' },
836+
{
837+
default: () => createComponent(Foo),
838+
},
839+
)
840+
},
841+
}).render()
842+
expect(html()).toBe('<!--async component-->')
843+
844+
await nextTick()
845+
resolve!(
846+
defineVaporComponent({
847+
name: 'Foo',
848+
setup() {
849+
onActivated(() => {
850+
spy()
851+
})
852+
return template('Foo')()
853+
},
854+
}),
855+
)
856+
857+
await timeout()
858+
expect(html()).toBe('Foo<!--async component-->')
859+
expect(spy).toBeCalledTimes(1)
860+
})
764861
})

packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
createIf,
2323
createTemplateRefSetter,
2424
createVaporApp,
25+
defineVaporAsyncComponent,
2526
defineVaporComponent,
2627
renderEffect,
2728
setText,
@@ -30,6 +31,7 @@ import {
3031
} from '../../src'
3132

3233
const define = makeRender()
34+
const timeout = (n: number = 0) => new Promise(r => setTimeout(r, n))
3335

3436
describe('VaporKeepAlive', () => {
3537
let one: VaporComponent
@@ -1045,7 +1047,81 @@ describe('VaporKeepAlive', () => {
10451047
})
10461048
})
10471049

1048-
test.todo('should work with async component', async () => {})
1050+
test('should work with async component', async () => {
1051+
let resolve: (comp: VaporComponent) => void
1052+
const AsyncComp = defineVaporAsyncComponent(
1053+
() =>
1054+
new Promise(r => {
1055+
resolve = r as any
1056+
}),
1057+
)
1058+
1059+
const toggle = ref(true)
1060+
const instanceRef = ref<any>(null)
1061+
const { html } = define({
1062+
setup() {
1063+
const setRef = createTemplateRefSetter()
1064+
return createComponent(
1065+
VaporKeepAlive,
1066+
{ include: () => 'Foo' },
1067+
{
1068+
default: () => {
1069+
return createIf(
1070+
() => toggle.value,
1071+
() => {
1072+
const n0 = createComponent(AsyncComp)
1073+
setRef(n0, instanceRef)
1074+
return n0
1075+
},
1076+
)
1077+
},
1078+
},
1079+
)
1080+
},
1081+
}).render()
1082+
1083+
expect(html()).toBe(`<!--async component--><!--if-->`)
1084+
1085+
resolve!(
1086+
defineVaporComponent({
1087+
name: 'Foo',
1088+
setup(_, { expose }) {
1089+
const count = ref(0)
1090+
expose({
1091+
inc: () => {
1092+
count.value++
1093+
},
1094+
})
1095+
1096+
const n0 = template(`<p> </p>`)() as any
1097+
const x0 = child(n0) as any
1098+
renderEffect(() => {
1099+
setText(x0, String(count.value))
1100+
})
1101+
return n0
1102+
},
1103+
}),
1104+
)
1105+
1106+
await timeout()
1107+
// resolved
1108+
expect(html()).toBe(`<p>0</p><!--async component--><!--if-->`)
1109+
1110+
// change state + toggle out
1111+
instanceRef.value.inc()
1112+
toggle.value = false
1113+
await nextTick()
1114+
expect(html()).toBe('<!--if-->')
1115+
1116+
// toggle in, state should be maintained
1117+
toggle.value = true
1118+
await nextTick()
1119+
expect(html()).toBe('<p>1</p><!--async component--><!--if-->')
1120+
1121+
toggle.value = false
1122+
await nextTick()
1123+
expect(html()).toBe('<!--if-->')
1124+
})
10491125

10501126
test('handle error in async onActivated', async () => {
10511127
const err = new Error('foo')
@@ -1193,7 +1269,39 @@ describe('VaporKeepAlive', () => {
11931269
})
11941270

11951271
describe('vdom interop', () => {
1196-
test('render vdom component', async () => {
1272+
test('should work', () => {
1273+
const VdomComp = {
1274+
setup() {
1275+
onBeforeMount(() => oneHooks.beforeMount())
1276+
onMounted(() => oneHooks.mounted())
1277+
onActivated(() => oneHooks.activated())
1278+
onDeactivated(() => oneHooks.deactivated())
1279+
onUnmounted(() => oneHooks.unmounted())
1280+
return () => h('div', null, 'hi')
1281+
},
1282+
}
1283+
1284+
const App = defineVaporComponent({
1285+
setup() {
1286+
return createComponent(VaporKeepAlive, null, {
1287+
default: () => {
1288+
return createComponent(VdomComp)
1289+
},
1290+
})
1291+
},
1292+
})
1293+
1294+
const container = document.createElement('div')
1295+
document.body.appendChild(container)
1296+
const app = createVaporApp(App)
1297+
app.use(vaporInteropPlugin)
1298+
app.mount(container)
1299+
1300+
expect(container.innerHTML).toBe(`<div>hi</div>`)
1301+
assertHookCalls(oneHooks, [1, 1, 1, 0, 0])
1302+
})
1303+
1304+
test('with v-if', async () => {
11971305
const VdomComp = {
11981306
setup() {
11991307
const msg = ref('vdom')

packages/runtime-vapor/src/apiDefineAsyncComponent.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createAsyncComponentContext,
66
currentInstance,
77
handleError,
8+
isKeepAlive,
89
markAsyncBoundary,
910
performAsyncHydrate,
1011
useAsyncComponentState,
@@ -28,6 +29,7 @@ import {
2829
import { invokeArrayFns } from '@vue/shared'
2930
import { type TransitionOptions, insert, remove } from './block'
3031
import { parentNode } from './dom/node'
32+
import type { KeepAliveInstance } from './components/KeepAlive'
3133
import { setTransitionHooks } from './components/Transition'
3234

3335
/*@ __NO_SIDE_EFFECTS__ */
@@ -122,7 +124,7 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
122124
// already resolved
123125
let resolvedComp = getResolvedComp()
124126
if (resolvedComp) {
125-
frag!.update(() => createInnerComp(resolvedComp!, instance))
127+
frag!.update(() => createInnerComp(resolvedComp!, instance, frag))
126128
return frag
127129
}
128130

@@ -149,8 +151,6 @@ export function defineVaporAsyncComponent<T extends VaporComponent>(
149151
load()
150152
.then(() => {
151153
loaded.value = true
152-
// TODO parent is keep-alive, force update so the loaded component's
153-
// name is taken into account
154154
})
155155
.catch(err => {
156156
onError(err)
@@ -193,6 +193,14 @@ function createInnerComp(
193193
appContext,
194194
)
195195

196+
if (parent.parent && isKeepAlive(parent.parent)) {
197+
// If there is a parent KeepAlive, let it handle the resolved async component
198+
// This will process shapeFlag and cache the component
199+
;(parent.parent as KeepAliveInstance).cacheComponent(instance)
200+
// cache the wrapper instance as well
201+
;(parent.parent as KeepAliveInstance).cacheComponent(parent)
202+
}
203+
196204
// set transition hooks
197205
if ($transition) setTransitionHooks(instance, $transition)
198206

packages/runtime-vapor/src/block.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ export function remove(block: Block, parent?: ParentNode): void {
157157
if (block.anchor) remove(block.anchor, parent)
158158
if ((block as DynamicFragment).scope) {
159159
;(block as DynamicFragment).scope!.stop()
160+
const scopes = (block as DynamicFragment).keptAliveScopes
161+
if (scopes) {
162+
scopes.forEach(scope => scope.stop())
163+
scopes.clear()
164+
}
160165
}
161166
}
162167
}

packages/runtime-vapor/src/component.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ import {
8686
} from './dom/hydration'
8787
import { _next, createElement } from './dom/node'
8888
import { type TeleportFragment, isVaporTeleport } from './components/Teleport'
89-
import type { KeepAliveInstance } from './components/KeepAlive'
89+
import {
90+
type KeepAliveInstance,
91+
findParentKeepAlive,
92+
} from './components/KeepAlive'
9093
import {
9194
insertionAnchor,
9295
insertionParent,
@@ -688,7 +691,7 @@ export function mountComponent(
688691
anchor?: Node | null | 0,
689692
): void {
690693
if (instance.shapeFlag! & ShapeFlags.COMPONENT_KEPT_ALIVE) {
691-
;(instance.parent as KeepAliveInstance).activate(instance, parent, anchor)
694+
findParentKeepAlive(instance)!.activate(instance, parent, anchor)
692695
return
693696
}
694697

@@ -723,7 +726,7 @@ export function unmountComponent(
723726
instance.parent.vapor &&
724727
instance.shapeFlag! & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
725728
) {
726-
;(instance.parent as KeepAliveInstance).deactivate(instance)
729+
findParentKeepAlive(instance)!.deactivate(instance)
727730
return
728731
}
729732

0 commit comments

Comments
 (0)