Should be able to read an observable without creating a subscription #326

Merged
merged 1 commit into from Jun 13, 2012

Projects

None yet

5 participants

@mbest
Member
mbest commented Feb 13, 2012

There are times when it's useful to read an observable without creating a subscription. Here's a simple example:

var counter = ko.observable(0), somethingToWatch = ko.observable();
var somethingDoubled = ko.computed(function() {
    counter(counter()+1);
    return somethingToWatch() * 2;
});

The call to counter() creates a subscription, but since we don't use the value except to write to counter, we shouldn't have a subscription. In fact this code will cause an stack overflow because writing to counter will trigger re-running the function.

In #225, I suggested adding a _latestValue property to observables so the value could be accessed when inspecting the observable in a debugger. This could be extended by exporting the property, and then it'd be possible to read the value directly without triggering a subscription. Writing to the property would not be supported, of course.

Another solution would be to add a function to observables like getWithoutDependency that returns the value.

Outside of modifying Knockout, a way to solve the example above is to update counter asynchronously in a setTimeout call: setTimeout(function() { counter(counter()+1); }, 0);

@mbest
Member
mbest commented Feb 13, 2012

Another solution is this (same idea as setTimeout, but not asynchronous): ko.ignoreDependencies(function() { counter(counter()+1); }); ignoreDependencies would cause any subscriptions in the inner function to be ignored. I've already created such a function in my smart-binding update, but I didn't export it.

@RoyJacobs
Contributor

I like the 'ignore dependencies' wrapper, since it doesn't rely on exposing internals.

@rniemeyer
Member

Not saying that it is a better option or that it couldn't stand to be directly accessible, but here is how I have done something like this in the past without changes to core:

var backdoorObservable = function(initialValue) {
    var _value = initialValue,
        result = ko.observable(initialValue);

    result.peek = function() { return _value; };

    result.subscribe(function(newValue) {
        _value = newValue;
    });

    return result;
};
@cspotcode

I personally like adding a .peek() method to all observables. This method returns _lastValue without registering a dependency, allowing simple read-only access without exposing any knockout internals.

@mbest mbest #326 - Access observables within a computed without subscribing:
Add `peek` function to observable and computed that returns value without subscribing.
Add `ko.ignoreDependencies` function that calls another function, ignoring any observables it accesses.
ed76234
@mbest
Member
mbest commented May 12, 2012

Ready to merge:

Add peek function to observable and computed that returns value without subscribing.
Add ko.ignoreDependencies function that calls another function, ignoring any observables it accesses.

@mbest mbest was assigned May 31, 2012
@mbest
Member
mbest commented Jun 6, 2012

If you're wondering why I've implemented two ways to avoid subscriptions, they have quite different use-cases:

  • peek is for handling things like #403 where the observable access is direct and controlled.
  • ignoreDependencies is for handling things like #341 where the observable access is in a callback function.
@SteveSanderson
Contributor

I'm pretty happy with the peek notion.

Still not sure about the requirement for ignoreDependencies. Generally, the dependency detection mechanism is very powerful but also prone to cause difficulties if people try to interfere with it, so the minimum number of ways of modifying its behavior is preferable - it's easier to reason about if there are fewer possibilities for what might be happening.

Are you saying that the core framework would need to make use of ignoreDependencies somewhere? If so, where? And in the case where someone working outside the core really needed that effect, wouldn't it be equivalent to the following?

ko.computed(function() {
    // code here will not be dependency-tracked
}).dispose();

Is #341 the main use case?

If you think it would be sufficient to simplify this just to peek I'd be happy to take this right away. Thanks!

@mbest
Member
mbest commented Jun 13, 2012

Steve,

Thanks for looking this over. ignoreDependencies is useful for wrapping any kind of callback function for which one doesn't want to track dependencies. In addition to #341 (which is important anyway), I plan to use it to prevent tracking dependencies from binding's init functions. The alternative you present is usable, but not very concise or obvious.

@SteveSanderson
Contributor

The alternative you present is usable, but not very concise or obvious.

OK, then let's minimise the API surface by omitting ignoreDependencies at least until some stronger requirement emerges in the future. I can update this pull request, or would you prefer to?

I plan to use it to prevent tracking dependencies from binding's init functions

I'm assuming you mean in a plugin, since that would be a breaking change in core.

@mbest
Member
mbest commented Jun 13, 2012

I've also used ignoreDependencies in a custom function I'm using that uses a computed to track multiple dependencies and perform an action based on them:

function updateOnChange(valueAccessor, callback) {
    return ko.computed(function() {
        var value = ko.utils.unwrapObservable(valueAccessor());
        ko.ignoreDependencies(callback, null, [value]);
    });
};
@mbest
Member
mbest commented Jun 13, 2012

I'm assuming you mean in a plugin, since that would be a breaking change in core.

Are you saying that tracking dependencies in init is a feature? Because if it is, it's not a very good feature. Once the binding is updated, that dependency is lost anyway since init isn't called again. I think that it's a bug. Breaking bugs is good for us.

@mbest
Member
mbest commented Jun 13, 2012

Are you willing to include ignoreDependencies, but not export it? That would be okay for me for now. That would just mean taking out this line:

ko.exportSymbol('ignoreDependencies', ko.ignoreDependencies = ko.dependencyDetection.ignore);

It could then be used internally through ko.dependencyDetection.ignore.

@mbest
Member
mbest commented Jun 13, 2012

We'd also have to take out the spec for ignoreDependencies if it wasn't exported. If you don't want to include it right now, that's okay I guess. But I think it will useful as part of #341 anyway.

@SteveSanderson
Contributor

Are you saying that tracking dependencies in init is a feature?

I'm only saying that removing it might break someone's code. Certainly you'd have to be doing something quite weird to be affected (like this: http://jsfiddle.net/T3Vdn/, which would get broken). Perhaps this is too obscure to regard as a true breaking change, and perhaps the benefits of removing it outweigh the costs, but that's a separate discussion.

We'd also have to take out the spec for ignoreDependencies if it wasn't exported. If you don't want to include it right now, that's okay I guess.

If a situation arises where we really need it in core, it could go in. But to keep things lean, can we omit it until such a situation does arise?

But I think it will useful as part of #341 anyway

I was thinking we could fix that just by moving the beforeChange notification (and also the _latestValue write) to run outside the ko.computed block, without needing any new infrastructure. Do you anticipate any drawbacks to that approach?

Sorry if I'm sounding reticent. I'm just trying to keep things in core as tight and minimal as they possibly can be.

@mbest
Member
mbest commented Jun 13, 2012

Okay, let's leave it out for now. When the time comes to discuss #341, we can look at all of our options for fixing it.

@mbest
Member
mbest commented Jun 13, 2012

Would you like me to merge the peek code or will you?

@SteveSanderson SteveSanderson merged commit ed76234 into master Jun 13, 2012
@SteveSanderson
Contributor

Merged

@mbest
Member
mbest commented Aug 20, 2014

With 3.2, you can now use pureComputed to run code within a computed without creating a dependency and without having to call dispose.

ko.pureComputed(function() {
    // code here will not be dependency-tracked
});

This also better supports the cases where you want to return a value from the function:

var value = ko.pureComputed(function() {
    // code here will not be dependency-tracked
    return thevalue;
}).peek();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment