Vapor: onUnmounted never fires on app.unmount(), while onBeforeUnmount and onScopeDispose do
### Vue version
3.6.0-rc.2
### Link to minimal reproduction
https://play.vuejs.org/#eNrFVF1v0zAU/SsXvzSR2gQxxEOVFrYxCZD40DZ4wQhlyW3rzbEtf4SiKv8d2UnTtNuKxgtPbe8599zjU19vyKlSSe2QTElmCs2UBYPWqTkVrFJSW9hAoTG3eKrUuPv6LVdSh99SfJROWCzH4AxeY6V4bvESF9DAQssKRrXDUa8VGs9XjJcdmqS7UrJPLWV1wNxW9okb4HLZj0tSLpfJrRlRQUUhhbFQ+wnvpLEwOzAZjXpsFPf0UlaPsTvIkzl2yqdKwQyE47wrlrIa1qjoQ4qiGGZz2FABw979UKNdIvGQmFReJOodJ3XOHbaUfmT/V0V9WkPGVqM7Ry/RxN7nwonCMinAicA7k3YVxft2XycdGA11D6uHucC9VBoqsrS9cHM/O7Nd0HPPzm6ctVLAm4Kz4m5GycARJfMi5xxypXZTQQq4kXYFxt1McqVMlrYSrVzJatC4mFHSx0fJPEtLVt8jdNns4wrqCfPwM3+9OIplMBIFf2BXCO24OEtV2+F4+ATIOIN6spB6RglnAoEJf2Mpgekd/u6KlMw3Gwhw02QpZ61IGlSydBAOGZP9lTnc3Db94f5KcYYLqfFrm5bf2qtCKnzLjJIG/e8OwvLhvT2yYwfi3RX3BOXMqtuw6aGHURz71n7s4209pWsZOn+8a8gKjccum1G5mIdWKHyqWRoqDwQ/fIGOvZj/N/FSVlN4euLbtqclvu36h8RLWf0l8PbUZEr6VDTmhWU1HqRGBa4Do33DfXaznht9/xEHOWsKKRbMa0pBpiS8bJQUslKMo/6s/OtnKJm2b57Hcs7lrw+hZrXD8bZerLC4e6B+a9a+RskXjQZ1jZT0mM31Em0LX1x9wrUdgJUsHcej4CUayZ332NLOnCg56gEvuH0fsmJieW0u1haF2R7KG/XMZtyeu3Z4fuToO7snycvQR0VDxuRnjdprkik5SV4lzye6SF6Q5g+hGuJd
### Steps to reproduce
The reproduction mounts two sub-apps side by side from one host component: `VaporChild.vue` is `<script setup vapor>`, `VdomChild.vue` is the same file without the attribute. Each registers `onBeforeUnmount`, `onUnmounted` and `onScopeDispose`, and each pushes into a shared array.
1. Click **"call app.unmount() on both sub-apps"**.
The list under the button shows which hooks ran.
### What is expected?
Both renderers run all three hooks, which is what the vdom sub-app does:
```
vdom: onBeforeUnmount
vdom: onUnmounted
vdom: onScopeDispose
```
### What is actually happening?
The vapor sub-app runs two of the three - `onUnmounted` never fires:
```
vapor: onBeforeUnmount
vapor: onScopeDispose
```
Measured against 3.6.0-rc.2, four cases, `<script setup lang="ts" vapor>` on every vapor fixture:
| teardown path | `onBeforeUnmount` | `onUnmounted` | `onScopeDispose` |
|---|---|---|---|
| vapor, `app.unmount()`, root component | fires | **never** | fires |
| vapor, `app.unmount()`, descendant component | fires | **never** | fires |
| vapor, `v-if` removing a child | fires | fires | fires |
| vdom, `app.unmount()`, root or descendant | fires | fires | fires |
So it is specific to `app.unmount()` under vapor: the same component's `onUnmounted` fires when it is removed by a `v-if`, and its siblings `onBeforeUnmount` and `onScopeDispose` fire on every path.
### Why it matters downstream
`onUnmounted` is where a component releases what it attached to something outside its own tree - a global listener, an observer, a timer, an entry in a shared registry. `app.unmount()` is the teardown path a test harness and an embedded widget use, and it is the one path where that release is skipped. Nothing warns: the app disappears from the DOM and the leak is silent until a later mount reads state the previous instance never cleaned up. We hit it porting a component that removed a document-level listener in `onUnmounted`; every test passed and the listener survived the unmount.
`onScopeDispose` is a working substitute, which is what we moved to. Filing because the divergence from the vdom renderer looks unintended rather than because we need a workaround.
1 条评论