Extending Knockout observables.

345 views
Skip to first unread message

Brian Di Palma

unread,
Nov 19, 2012, 10:30:10 AM11/19/12
to knock...@googlegroups.com
Hello,

I was wondering if I provided a patch to Knockout, that allowed the Knockout observables to be objects instead of functions, is there any possibility
of it being folded into Knockout?

Currently the only way to achieve this is to modify Knockout.

So for instance instead of,

        unwrapObservable: function (value) {
            return ko.isObservable(value) ? value() : value;
        },

, use this instead,

        unwrapObservable: function (value) {
            return ko.isObservable(value) ? value.getValueForKnockout() : value;
        },

Of course in it's present state this code would need to check for the function observables otherwise it would break those.

Instead of,

        writeValueToProperty: function(property, allBindingsAccessor, key, value, checkIfDifferent) {
            if (!property || !ko.isWriteableObservable(property)) {
                var propWriters = allBindingsAccessor()['_ko_property_writers'];
                if (propWriters && propWriters[key])
                    propWriters[key](value);
            } else if (!checkIfDifferent || property.peek() !== value) {
                property(value);
            }
        }

, have something like this,

        writeValueToProperty: function(property, allBindingsAccessor, key, value, checkIfDifferent) {
            if (!property || !ko.isWriteableObservable(property)) {
                var propWriters = allBindingsAccessor()['_ko_property_writers'];
                if (propWriters && propWriters[key])
                    propWriters[key](value);
            } else if (!checkIfDifferent || property.peek() !== value) {
                property.setUserEnteredValue(value); <--- change here
            }
        }

, again this would break the function observables but it's trivial to support both.

This is due to using a Presentation Model that is generic as opposed to specific to Knockout and also because using the prototype reduces memory usage in browsers as opposed
to functions closing over each other, this helps performance with long lived and complex applications.

Here is a snippet from an object based observable.

view.knockout.KnockoutObservable = function()
{
    /** @private */
    this.m_bSubscribed = false;
   
    /** @private */
     this._subscriptions = {};
   
    /** @private */
    this.__ko_proto__ = ko.observable;
};

/** @private */
view.knockout.KnockoutObservable.prototype.subscribe = function (callback, callbackTarget, event)
{
    this._addKnockoutObservableAsListenerIfNotSubscribed();
   
    var boundCallback = callbackTarget ? callback.bind(callbackTarget) : callback;
    event = event || "change";
   
    if (!this._subscriptions[event])
    {
        this._subscriptions[event] = [];
    }
   
    this._subscriptions[event].push(boundCallback);
   
    return new view.knockout.KnockoutSubscription(boundCallback, this, event);
};

/** @private */
view.knockout.KnockoutObservable.prototype.notifySubscribers = function (valueToNotify, event)
{
    event = event || "change";
   
    if (this._subscriptions[event])
    {
        ko.dependencyDetection.ignore(function() {
            ko.utils.arrayForEach(this._subscriptions[event].slice(0), function (subscription) {
                // In case a subscription was disposed during the arrayForEach cycle, check
                // for isDisposed on each subscription before invoking its callback
                if (subscription && (subscription.isDisposed !== true))
                {
                    subscription(valueToNotify);
                }
            });
        }, this);
    }
};

Obviously if this has no chance of being integrated into Knockout then there is no need to make a branch and pull request but I'd be happy to do so
if this is something that will be accepted to Knockout, the number of changes is relatively minor as the Knockout code has a minimal surface area
contact with the observables beyond calling the methods which does not require any change to the Knockout code.

B.

gaffe

unread,
Dec 3, 2012, 5:42:58 PM12/3/12
to knock...@googlegroups.com, off...@gmail.com
Not sure I understand this.. functions in javascript are objects already?

off...@gmail.com

unread,
Dec 4, 2012, 5:43:57 AM12/4/12
to knock...@googlegroups.com, off...@gmail.com
Hi gaffe,

Yes, I wasn't very clear in my description.

Currently a KO observable is simply a function that you can call to retrieve or set the observed model value.
Attached directly onto that function are other functions. These can be called to subscribe to the observable
and query how many subscriptions there are to the observable etc.

Each time a KO observable is created all these function bodies are copied by the browser as the browser has
no way of optimizing this object creation, it doesn't know that these functions will stay the same so it doesn't need
to create a copy of the functions each time.

Apart from that issue it does mean these observables aren't open for extension.

What I proposed was to use observables which are open for extension by having observables with normal constructors
and using the prototype chain, this also has the advantage that each observable will use less memory as functions
on the prototype will not be cloned each time you use the constructor.

The observables would be in the style I gave above.

view.knockout.KnockoutObservable = function()
{   
    /** @private */

     this._subscriptions = {};
   
    /** @private */
    this.__ko_proto__ = ko.observable;
};

/** @private */
view.knockout.KnockoutObservable.prototype.subscribe = function (callback, callbackTarget, event)
{
//code.
}

This class basically pretends to be a normal KO observable function with attached functions.

The advantages for me are

1) It's OOP.
2) The objects take less memory.
3) My code is not coupled to KO.

To explain 3; most people when using KO write code like this for their observables

this.age = ko.observable(10);

This means that their code is tightly coupled to KO.

What I would tend to do instead is this.

this.age = presentation.model.Property(10);

with the presentation.model.Property class being my own presentation class.

Now how I get KO to accept this weird observable is I extend my view.knockout.KnockoutObservable class inside my presentation.mode.Presentation class.

I wrote the documentation that explains how to do extension on MDN ( https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create )
if you want to read up on it.

This means I have very loose coupling to KO for my presentation code and could in theory easily substitute out KO for Angular or another framework if need be.
This allows me to support clients who want to use whatever framework they are familiar with as opposed to imposing one on them.

I basically have one line in my Presentation class that says it extends the KO observable class I created.

This does mean I must make a few changes to KO though for it to use methods on my class instead of simply calling the object as if it where a function

i.e. value.getValueForKnockout() instead of value()

The changes are minor though. I would like to have support for class based observables in KO if I could though. Does this help explain my question better?

B.

Enis Pilavdzic

unread,
Dec 4, 2012, 8:54:44 AM12/4/12
to knock...@googlegroups.com
I see. All I can tell you is that knockout uses extenders instead of doing it that way, one of the contributors might be able to explain better why this is so I'm not sure.
--
 
 

Miguel Castillo

unread,
Dec 4, 2012, 12:46:38 PM12/4/12
to knock...@googlegroups.com, off...@gmail.com
I would be really interested in seeing some profiling on this to just have concrete numbers we can look at.  Just stating that one approach is more memory efficient than the other generally isn't sufficient.  The question shortly after is... Well, how much memory will we get out of this change?

off...@gmail.com

unread,
Dec 4, 2012, 2:55:01 PM12/4/12
to knock...@googlegroups.com, off...@gmail.com
Hi Miguel,

I don't have any benchmarks currently at hand. I can ask my workplace if some quick ones can be run or I may try and set up some myself.

It does look as if it's not only more memory efficient but faster to use the prototype.

http://stackoverflow.com/questions/3493252/javascript-prototype-operator-performance-saves-memory-but-is-it-faster

I wasn't suggesting that I wanted to have class based KO observables as default, the current ones clearly work well for the majority of KO users.
I was just wondering if support for both styles of observers could be added, in essence the major difference between the two types comes at reading
and writing of the data to the model. Apart from those points there is no difference between the two styles of observers.

For the current style you would read and write to the model by calling the observer function :

var _latestValue = value()

value("newValue")

While in the class based observer you would have methods.

var _latestValue = value.getValueForKnockout()

value.setUserEditableValue("newValue")

Apart from that

value.subscribe(observable)

etc would be the same. I think that the performance improvements would be mainly felt in the older IE versions more than the newer browsers.

To be honest the major driver for us to make this change to our copy of Knockout was the fact that we were not tightly coupled to a
specific binding framework and that we could more easily change if required.

I'm currently on holiday so I'm going to see if someone from my workplace can run some tests or provide you with code.
The code snippets that I've already provided do basically show the extent of the changes we've had to make an our class based
observable, it really isn't a huge class/change. I've mainly copied the code from the observable in KO into a prototype using class.

If you are interested in what we are doing with KO you can read our website

http://www.caplin.com/developer/component/presenter

The Property class is the one that extends the class based Knockout observable.

If you have any questions fire away!

Brian.

off...@gmail.com

unread,
Dec 16, 2012, 8:18:45 AM12/16/12
to knock...@googlegroups.com, off...@gmail.com

Sorry for the delay in replying. There doesn't seem much enthusiasm for picking this task up in my office.

I think it might be down to the fact that the changes are quite small and self contained.
KnockoutJS encapsulates the reading and writing from observables in a few locations which makes our changes
easy to integrate when we upgrade. So I don't think there is much worry about needing our changes integrated into KO.

Again sorry for the delay, hopefully this post is useful for people who wish to have loose coupling to KO.

Brian.
Reply all
Reply to author
Forward
0 new messages