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

Is there a way to debounce computed functions? #48

Closed
dzearing opened this issue Nov 11, 2015 · 10 comments
Closed

Is there a way to debounce computed functions? #48

dzearing opened this issue Nov 11, 2015 · 10 comments

Comments

@dzearing
Copy link
Contributor

Say I have a computed observable:

@obserable a: string;
@observable b: string;

@observable get computeTitle() {
  return a + b;
}

If I assign both a and b:

a = 1;
b = 1;

Then computeTitle is called 2 times. This is a waste and definitely adds up in bulk usage. I would like it to be called once, or on demand if it's needed. E.g.

a = 1; // nobody fetched computeTitle, but lets mark it as dirty
b = 2; // nobody fetched computeTitle, but again lets mark it

Then... right before we update react observer components, if there are observers of a dirty computed, we evaluate it so that we can know if we need to tell the observers.

In Knockout, I think there are 2 tools to deal with this. Pure computeds can be used for scenarios like this where there are no side effects of the function being called. If nobody accesses their value, I believe they don't even calculate. I could be wrong though.

Rate limiting is another tool to prevent chatty observables from causing redundant re-evaluations of computed.

Both have their purposes.

Is there anything in mobservable for this? I'd love by default that computed functions do the optimal thing and that you have to do extra work to do suboptimal things (like have the function called on every granular re-evaluation.)

I have heard that there are transactions you can do around assigning multiple things to avoid redundant computes, but didn't find docs on it and haven't dug through the code yet.

@mweststrate
Copy link
Member

Hi @dzearing,

There is indeed a transaction mechanism, in your case it would look like:

mobservable.transaction(() => {
   a = 1;
    b = 1;
});

To achieve the desired effect; e.g. recompute computeTitle() only once.

Further note that computeTitle won't compute at all as long as there are no active observers, which is a very nice optimization.

If you don't observe computeTitle but still ask for its value in your code, the computeTitle function will just evaluate lazily (on demand). This might result in more computations then strictly needed (effectively, like if there wasn't an @observable annotation) as long as you don't actively observe the value (using autorun or @observer), but at least it gives always consistent answers.

@dzearing
Copy link
Contributor Author

I see. The transaction support is great and that feels super useful.

It would be nice to have computed function evals be optionally throttled to prevent chatty situations, even when there are observers. I guess we could do this like so:

@observable get title() {

  throttle(() => { 
    this._lastTitle = this.a + this.b;
  }, 1000);

  return this._lastTitle;
}

However this isn't really desired. If someone gets title, it should be synchronously resolved. In the example above, accessing title will return the old value rather than the new. If a and b mutate, we simply want the observers to be notified when N idle time has passed. I think this has to be provided at the notifier layer.

Something like:

@observable({ throttle: 1000, rateLimit: 2000 }) get title() { ... }

@mweststrate
Copy link
Member

There is an autorunAsync method (darn, didn't put it in the docs I notice now 😞 ), anyway, it's definition is as follows:

/**
    * Once the view triggers, effect will be scheduled in the background.
    * If observer triggers multiple times, effect will still be triggered only once, so it achieves a similar effect as transaction.
    * This might be useful for stuff that is expensive and doesn't need to happen synchronously; such as server communication.
    * Afther the effect has been fired, it can be scheduled again if the view is triggered in the future.
    * 
    * @param view to observe. If it returns a value, the latest returned value will be passed into the scheduled effect.
    * @param the effect that will be executed, a fixed amount of time after the first trigger of 'view'.
    * @param delay, optional. After how many milleseconds the effect should fire.
    * @param scope, optional, the 'this' value of 'view' and 'effect'.
    */
export function autorunAsync<T>(view: () => T, effect: (latestValue : T ) => void, delay:number = 1, scope?: any): Lambda {

Using this is a bit more verbose / ugly then your proposal, it is an interesting proposal. On the other hand, it might be very powerful as well to just provide interoperability with RxJs (which should be quite easy to achieve), because that library already provides excellent support for time controlled observable values.

@sunny-g
Copy link

sunny-g commented Nov 13, 2015

I was trying to use transactions to achieve the same kind of debouncing, to no avail. Maybe I am using it wrong but here's the code I have:

var array = mobservable.observable([])
array.push({ keys1: {1: true}})
array.push({ keys2: {2: true}})

array.observe((changes) => {
  console.log('changes in array:', changes);
});

mobservable.transaction(() => {
  var obj = array.splice(0, 1)[0];
  array.push(obj);
});
// 'changes in array' logs twice

@mweststrate
Copy link
Member

Hi @sunny-g

Transaction only works for autorun or observable functions. It doesn't limit events fired by array.observe / mobservable.observe to prevent "missing" changes. If you replace array.observe with mobservable.autorun(() => console.log('updated array', array.join(', '))) for example it would print only once during the transaction.

@sunny-g
Copy link

sunny-g commented Nov 13, 2015

Hey @mweststrate, ahh, makes sense. As it turns out, the autorun workaround works just fine for my case. Thanks for your help and for writing such a powerful library!

@mweststrate
Copy link
Member

Closing this issue for now, might need re-evaluation after implementing #57.

@sunny-g
Copy link

sunny-g commented Nov 19, 2015

#57!

I was planning on incorporating RxJS observables for just one part of my code (I have multiple consecutive websocket messages coming in, both mutate the same mobservable, but I want them combined into one atomic action so they only mutate the mobservable once).

I don't think it makes any sense to wrap my socket message handler into a transaction, and current approach is to turn the websockets into an RxJS observable that I can debounce.

Based on what I've proposed, is this something I can do with mobservable as is, or would I need RxJS?

@mweststrate
Copy link
Member

You can build some manual debounce around mobservable, but personally I would use RxJS indeed, apply the debouncing etc and then observe the resulting stream to update the mobservable data. For anything that involves working with time such as throttling, or which involves combining multiple events from the same stream (and not just the latest) I would use RxJs which provides the more low level primitives. For everything else, and especially for storing app state, I would use Mobservable which is IMHO more convienient and high level to work with.

@sunny-g
Copy link

sunny-g commented Nov 19, 2015

Thank you for your input, I'm really glad to hear I'm not using mobservable and RxJS incorrectly while also reinventing the wheel :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants