Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Declarative debouncing with proper dependency tracking #10974

Closed
mitar opened this issue Jan 2, 2020 · 4 comments
Closed

Declarative debouncing with proper dependency tracking #10974

mitar opened this issue Jan 2, 2020 · 4 comments

Comments

@mitar
Copy link
Contributor

@mitar mitar commented Jan 2, 2020

What problem does this feature solve?

If one wants to debounce recomputation when state changes too often, documentation offers an approach with watching. This approach has a major limitations though: it is imperative and thus not compossible very well (when multiple state fields are used). One has to know manually which reactive sources getAnswer uses and then setup watchers for each of the reactive fields manually. If code of getAnswer changes to use more of the state, watchers have to be updated as well. If getAnswer is really complicated (which is the main purpose of debouncing, a complicated and expensive function), calling potentially into other functions, it is not always clear what all state it is using.

What does the proposed API look like?

Ideally, debouncing throttling of expensive computed fields would be integrated with Vue reactivity enginge. Such computed property would:

  • Run the computation once, detect which reactive dependencies the computation uses.
  • When dependencies change, instead of scheduling computation to be re-run immediately, it would be queued with additional metadata: the timestamp only after which it should be run from the queue. For debouncing, new dependency changes would push further down the queue existing re-run entry (while keeping the timestamp in the future). For throttling, logic would just have to prevent re-adding an existing entry to the queue (I think this is already the case, so it would work out; we would just have to not run the entry before the timeout).
  • So current queue processing would just have to be changed so that entries which timeouts in the future are skipped when going over the queue. Leaving them for future queue processing to process them. In a way we would make the queue not be just a FIFO queue.

How we expose this to the developers, not sure, maybe we could expand on the getter/setter syntax of the computed fields, and add also some fields for debounce/throttling options.

@posva

This comment has been minimized.

Copy link
Member

@posva posva commented Jan 3, 2020

The debounce implementation was removed in v2 (#2873), so this would make sense as a plugin rather than implemented in core. After all, a declarative debounce serves a small set of use cases and lose flexibility. If implemented as a plugin, multiple properties can be injected to know if the property is awaiting for execution and such, bringing back the lost flexibility mention above, but this really belongs to a plugin.

This approach has a major limitations though: it is imperative and thus not compossible very well (when multiple state fields are used)

With the Composition API, you can now have a flexible and composable approach for this

@posva posva closed this Jan 3, 2020
@mitar

This comment has been minimized.

Copy link
Contributor Author

@mitar mitar commented Jan 4, 2020

I do not see a way how a plugin can currently manipulate the reactivity queue or determine which of reactive dependencies have been invalidated, without support in the core? Currently when a reactive dependency is invalidated, watchers are always scheduled to be run during the next tick.

So while I agree that plugins could be used to further customize exactly how often you want something to be recomputed, core changes are necessary. (Or point me to an example of such a plugin/or way to address what I am suggesting.)

From my investigations, without core changes, you cannot get Vue to be keep invalidating your reactive computation, without really running that computation. But the whole point is that you do not want to run it, because it is expensive. On other hand, you have to be kept invalidated so that you know how long to debounce for (because you want to rerun the reactive computation only X ms after the last invalidation).

@sirlancelot

This comment has been minimized.

Copy link
Contributor

@sirlancelot sirlancelot commented Jan 4, 2020

Debouncing as you're describing at the data level actually obscures information from the Application. The best way to debounce would be to have a data property that specifies what is happening and when so that your application can react to it. The Vue team provided a great example of how to do it well in their migration documentation which can be found here:

https://vuejs.org/v2/guide/migration.html#debounce-Param-Attribute-for-v-model-removed

@mitar

This comment has been minimized.

Copy link
Contributor Author

@mitar mitar commented Jan 4, 2020

@sirlancelot Will all respect, I have linked to existing Vue documentation which have a very similar example to the one which you linked to (in the migration guide). I am familiar with Vue, I know about debounce in 1.x and also proposed approach to it in 2.x. Please see my initial discussion in this issue where I describe the current approach in 2.x as problematic because it requires from you to manually (imperatively) declare what to watch. Moreover, I am not talking about debouncing at the data level, but at the reactive computation level.

Let's have a concrete example. Because maybe I am not explaining this well (and thus @posva suggested plugins and @sirlancelot suggested approach using watch).

So let's have that I have the following computed field:

computed: {
  translatedPost() {
     return this.translate(`${this.title}: ${this.body} (${this.comments})`);
  }
}

Now, let's imagine that calling translate is a very expensive operation. And as you see, the translatedPost is a reactive computation and has three reactive data sources: title, body, and comments. Because translate is expensive, I would like to debounce how often translatedPost is recomputed. Now, what are we debouncing on? We are debouncing on all three reactive sources: when any of them changes, it should push the recomputation for a later time. Only when all of them stop changing for X ms, we should recompute the translatedPost.

In Vue 2.x the recommended approach is very imperative and hard to keep correct as the codebase grows. I would have to:

  • Define a debounced version of the reactive computation above.
  • Create watch on title, body, and comments calling the debounced computation.

The problem with this is that I have to know what are all reactive sources. What if translate internally also uses this.language? Or if it starts using it in the future. I would have to manually add a watch to debounce when this.language changes. If I do not, not just that it does not debounce, it is not recompuated at all, when this.language changes. So it is very fragile imperative piece of code.

Now, how could this be done in a resuable way, inside our outside a plugin, I see no way. There is no supported way to programmatically extract reactive data sources from a reactive computation in Vue. I mean, it is in general not doable, because different code paths can use different reactive data sources.

But Vue reactivity system knows which reactive data sources were used and when any are changed, the reactive computation translatedPost is scheduled to be rerun. Now, the issue is that this is always scheduled to be rerun at the next tick and there is no way to influence that.

Am I missing something?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.