vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.7k stars 8.33k forks source link

Kept alive components still update (renders, watchers) while being deactivated #5386

Open posva opened 2 years ago

posva commented 2 years ago

Version

3.2.30

Reproduction link

sfc.vuejs.org/

Steps to reproduce

What is expected?

What is actually happening?

After changing the page once, the Page One keeps rendering while being inactive


This has been confusing to many people but I couldn't find if this behavior is expected or not

This also means watchers are executed in deactivated branches

It also worth noting that the component being deactivated renders before being deactivated. Making it impossible to skip the render with likely incorrect data.

BenceSzalai commented 1 year ago

I couldn't find if this behavior is expected or not

Can it be confirmed at least that these behaviour are indeed bugs and not intended?

From the layman's perspective they clearly are by the token that if someone wanted to keep the components rendering and watchers running they could use v-show to show/hide the component. The whole point of <KeepAlive> is to save resources in two ways: 1.) Avoid loosing the expensive/important state of the component, when switching away from it, as opposed to v-if and the like. 2.) Avoid taking resources for rendering and watching while in the background, as opposed to v-show="false".

The current behaviour matches that of option 2. Since it is already achievable quite easily with the v-show directive it suggest that it cannot be the goal of <KeepAlive>.

The documentation is rather vague on what the intended behaviour should be as it only really explains the behaviour as "cached". It explicitly says that the component state should be preserved (differentiating it from v-if etc.), but nothing about rendering, watchers or computed properties, which would differentiate it from v-show.

For me it looks like rendering, watchers or computed properties should be paused, otherwise we could simply just use v-show="false".

Correct me, if I'm wrong pls!

yxchai commented 1 year ago

The current behaviour matches that of option 2. Since it is already achievable quite easily with the v-show directive it suggest that it cannot be the goal of <KeepAlive>.

From my perspective, the rendering is not taken place as it's not rendered in the DOM tree, so it's not the same as v-show="false". While the official guide said below, I would rather say it's similar to v-show, but it won't stay in the DOM tree which means there will be less DOM manipulation(especially, if we are rendering large scale table, there will be a ton of DOM manipulation). Any maybe that's why it's said cache in the official guide. (cached in the virtual DOM, maybe...)

The difference is that an element with v-show will always be rendered and remain in the DOM; v-show only toggles the display CSS property of the element.

I also created a minimal playground locally but with Vue 2. From what I can tell, it works exactly the same as what @posva see in his playground.

So I suspect maybe the behavior is intended and not a bug. Let me know if you have different ideas. :-)

nicolas-t commented 1 year ago

Hello,

So I suspect maybe the behavior is intended and not a bug. Let me know if you have different ideas. :-)

Indeed if it's the same behavior as vue 2 then it might be intented, The problem is other issues showing clearly not expected behaviors have been closed in favor of this issue : https://github.com/vuejs/core/issues/5323 https://github.com/vuejs/core/issues/5207

BenceSzalai commented 1 year ago

I would rather say it's similar to v-show, but it won't stay in the DOM tree which means there will be less DOM manipulation

That is a good point. Thanks!

If I look from this perspective than I can see these activity levels a component in theory could be in:

Level Component state Calculated, Watch etc. Rendering In the DOM Visible Current solution
1st 💡 In memory 🔆 Run ✅ Do ✅ Yes ✅ Yes ✅ Built in: Base state of mounted, active component
2nd 💡 In memory 🔆 Run ✅ Do ✅ Yes ❌ No ✅ Built in: v-show="false"
3rd 💡 In memory 🔆 Run ✅ Do ❌ No ❌ No ✅ Built in: <KeepAlive> + v-if="false"
4th 💡 In memory 🔆 Run ⏸ Paused ❌ No ❌ No ❗️ Inconvenient1: v-memo
5th 💡 In memory 💤 Sleep ⏸ Paused ❌ No ❌ No ‼️ Almost practically impossible2: v-memo + abortable watchers
6th 💣 Destroyed 🕳 N/A 🕳 N/A ❌ No ❌ No ✅ Built in: v-if="false"

note 1: Achieving rendering to halt using v-memo can be a bit cumbersome: one can make a Ref updated by a watcher that hashes together all the attributes that would be passed in the v-memo array, but also observes a control variable which can be used to abort any update to the v-memo control Ref when false effectively halting the rendering. It would be still much more convenient to have a simple wrapper, like <KeepRunning> + v-if="false", because the approach with v-memo forces the developer to explicitly collect and specify all dependencies, which is tiresome, prone to errors and creates maintenance overhead.

note 2: The even bigger problem is: sometimes we need 5th state as well, but currently it is not achievable in any reasonably simple ways. In fact one can replace all calculated variables with watchers and make those watchers observe a bail-out variable to abort any updates when desired, but it is a huge overhead to do it this way, also it must be combined with the v-memo solution from 4th level to also pause the rendering... And I'm not even sure if that would pause calculated attributes of child components. If not, those would need to be modified as well to observe the control variable. One must be desperate to do this.

In fact the name "KeepAlive" suggests something other than 5th or 6th state, as 6th obviously cannot be considered "Alive" and 5th neither based on that Watchers, Calculated and Rendering are disabled. So implementing 3rd or 4th state under the name <KeepAlive> makes sense.

On the other hand if the explanation in the documentation says "Cached", why the component is called "Alive"? Further complicating the matters the associated component state is called "inactive", and the associated lifecycle hook is called "deactivated". I know it happens a lot that names evolve organically, but I'd suggest these 4 different names are adding to the confusion. Also the term "cached" would be the perfect terminology to differentiate 3rd and 5th state: <KeepAlive>: same as right now, <KeepCached>: same as <KeepAlive> but with rendering and watchers+calculated paused.

In an ideal world I'd love to see convenient ways to do all 6 levels:

BenceSzalai commented 1 year ago

Now the terminology is hard to get right, because obviously existing names should be kept, also the same things may have different name from Component lifecycle perspective, documentation, developer experience representation etc.

Anyway here's my take on trying to collect my thoughts:

Aspect removed/disabled Lifecycle term Additional commonly used terminology3 Ideal state terminology4 Related markup
- mounted visible mounted -
Visible mounted visible => hidden mounted => hidden v-show="false"
In the DOM activated => deactivated active =>
inactive / cached
(any above) => alive <KeepAlive> + v-if="false"
Rendering n/a n/a (any above) => running <KeepRunning> + v-if="false"
Calculated, Watch etc. n/a n/a (any above) => cached <KeepCached> + v-if="false"
Component state * => unmounted created => destroyed (any above) => destroyed v-if="false"

note 3: These are the terms I found in Vue Dev Tools and the Documentation.

note 4: IMHO, indeed.

tafelnl commented 1 year ago

I would argue your 5th 'level' describes the most logical and expected behaviour of them all, especially when combining it with vue-router. Suppose we have a simple app, with a "home"-page and another page (let's say "about us"). We use keep-alive in combination with vue-router to keep track of the entire state of the pages. If the user is on the "about us"-page, the "home"-page is invisible and vice versa.

Suppose the user navigated from "home" to "about us". Why would any developer want:

Am I missing a very obvious use-case here? The current behaviour of keep-alive - especially in combination with vue-router - just doesn't make any sense to me at all. Can anyone enlighten me?

yxchai commented 1 year ago

I would argue your 5h 'level' describes the most logical and expected behaviour of them all, especially when combining it with vue-router. Suppose we have a simple app, with a "home"-page and another page (let's say "about us"). We use keep-alive in combination with vue-router to keep track of the entire state of the pages. If the user is on the "about us"-page, the "home"-page is invisible and vice versa.

Suppose the user navigated from "home" to "about us". Why would any developer want:

* calculated stuff and watchers to continue on "home"? It just takes up user's resources (e.g. cpu and ram), and we're not going to show anything to the user based on these changing values. At most it can result in unexpected behaviour.

* rendering to continue on "home". It doesn't make sense, as the page is hidden anyways

Am I missing a very obvious use-case here? The current behaviour of keep-alive - especially in combination with vue-router - just doesn't make any sense to me at all. Can anyone enlighten me?

@tafelnl , I think it relates to this https://github.com/vuejs/core/pull/7286 pull request. I believe once this PR is merged. We will see this bug being fixed in vue-router soon.

tafelnl commented 1 year ago

@yxchai I think that's a totally different issue to be honest. I am talking about pausing computed and watch and pausing rendering after navigating away from a route. That issue talks about invoking the activated hook for child components

Alfred-Skyblue commented 1 year ago

I tried pausing the effect in the keep-alive deactivate state in #9206. It can be achieved currently, but it is more difficult in vShow because vShow needs to execute the effect before it can determine whether to display it.

angelov-a commented 3 months ago

The issue with keeping the state reactive is very much concerning the app's mode of operation.

We are using KeepAlive for routing and each view opens a modal on a specific url. The modal itself registers a keystroke listener for the Esc key which navigates back to a predetermined url. Now the deactivated view also detects that it needs to setup the modal, which upon pressing Esc triggers navigation to the wrong page (the deactivated one).

mefcorvi commented 3 months ago

I doubt that the current behavior will ever be changed because it would be a massive breaking change: there is no guarantee that there aren't projects that rely on reactivity continuing to work inside deactivated components.

The very concept of KeepAlive implies that a component is added to the vDOM/DOM faster because it is kept in memory. If reactivity within deactivated components stops working, then when the component is added back to the vDOM/DOM, it would be almost like creating the component anew, as all computed properties would need to be recalculated and the vDOM/DOM re-rendered.

Additionally, what should be done in situations where, for example, there is a watch in the component on a value from the Vuex store, and the component increments a counter every time this value changes? Should we remember all the changes and replay them upon reactivation of the component? This example is far-fetched, but in real applications, the logic can be much more complex.

It must be admitted that in our project, we did indeed encounter situations where reactivity within deactivated components (in our case, entire pages) led to performance drops. We solve this with two custom composables: useActivatableEffect and computedActivatable. All reactivity inside useActivatableEffect is removed when the component is deactivated and recreated upon activation. We also use this for registering event listeners. The computedActivatable function uses useActivatableEffect internally to create a computed property that only recalculates when the component is active.

I cannot share the code for these composables, mainly because we handle many different nuances related to SSR and hydration, and we also perform a lot of other necessary magic specific to our case. However, fundamentally, these are very simple functions. Here is an example implementation in the SFC Playground.

angelov-a commented 3 months ago

@mefcorvi Thanks for the explanation. I was thinking of using KeepAlive hooks to control reactivity, but thought that there may be a more general solution.

I see that #9651 implements pause/resume while mentioning KeepAlive although there are no changes in the PR related to the latter, as far as I can tell. Will the new methods have to be explicitly used (together with activated/deactivated) in order to achieve results similar to your examples?