Object.observe and observing "computed properties"

François REMY fremycompany_pub at yahoo.fr
Wed Aug 29 03:09:25 PDT 2012


Thanks for this analysis. I globally agree with your reasoning, but I don't 
think this is much of a problem.

Certainly, dependency analysis isn't completely effective, in the sense that 
each time you get 'out' of your 'observable world', you may miss updates. 
This is, however, not such a big issue as most binding languages feature 
that kind of 'issues', even your 'observe' API. This is just a fact that 
'Math.random()' and 'window.innerWidth' can't be observed, and it's up to 
the developer to make sure it they are not in their code, just like it's 
your reponsability to notify for accessors if you're using 'Object.observe'.

Most of the times, when you're binding your View to you Model, your model 
only lives in a particular world where it only interacts with other model 
objects, so that you can control completely the model world to make it 
observable, and you shouldn't have 'lost' dependency issues.

However, when there's no other possibilities (but it's really rare) than to 
use 'unobservable' properties, the only solution left to you is to use an 
event-based approach ('window.innerWidth + window.onresize') or perform 
polling ('Math.random() + setInterval'). My own JS library has something 
similar called 'binding bridges' which basically maps the properties of an 
object to its dynamic value, and updates them has necessary using a custom 
model (event or setInterval) :

    var BindingBridges = Object.makeBindable({});
    BindingBridges.add = function(name,evaluator,timespan) {
        BindableObject.methods.addProperty.call(this,name);
        BindingBridges[name] = evaluator();
        setInterval(function() {
            var cValue = evaluator();
            if (BindingBridges[name]==cValue) { return; }
            else { BindingBridges[name]=cValue; }
        }, timespan);
    }
    // you could also use an event-driven approach and
    // set BindingBridges.property to its new value at
    // each call of the event, instead.

Developers relying on unobservable things can use such mechanism to solve 
their issue. For example, in my 'mail' template application, the time was 
displayed as 'seconds ago', 'less than 1 minute ago', '? minutes ago', '? 
hours ago', ... and was relying on a 'BindingBridge.now' property which was 
updated using setInterval every 20 seconds or so.

As for the observation of an array, the classical 'dependency' pattern is 
sufficient, most of the times, since you'll depends on the 'array.length' 
property of your array (when an item is inserted or deleted, you'll be 
notified) and on the 'array[i]' value you touched.

    var o = {
        values: [0,1,5,-3],
        get max() {
            var max = Number.negativeInfinity;
            this.values.forEach(
                (i) => (i>max && max=i)
            );
            return max;
        }
    }

    Object.makeBindable(o);
    new Binding(=> o.max, v => $("#min").textContent=v);
    o.values.push(-10); // works
    o.value[o.values.length-1] = 3; // works

However, listening to a collection is something different from listening to 
a simple object. My own JS library used a BindableArray class which, indeed, 
generated 'insert' and 'delete' events in addition to the tradtionnal 
'update' events, as well as a HTMLForEachBinding which listened specifically 
to array events and managed the DOM as a close copy of the array using a 
binding template; those 'insert' and 'delete' events could be transferred to 
a 'made-observable' arrays.



-----Message d'origine----- 
From: Rafael Weinstein
Sent: Wednesday, August 29, 2012 7:28 AM
To: es-discuss at mozilla.org ; François REMY ; Steven Sanderson
Subject: Object.observe and observing "computed properties"

Steve Sanderson (author of the excellent KnockoutJS framework) and
François REMY both raised the issue of observing computed properties
WRT to the Object.observe() proposal.

Having thought about this problem while helping to author
Object.observe, my thoughts are as follows:

First, I think you need to look at the problem the right way.
"Observing computed properties" is kind of non-sensical. What this
translates to is "observe when the *return value* of an anonymous
function invocation *will be* different from the last invocation".

I think the only reasonable way to look at it is this: imagine that a
*data property* can be defined whose value can only be set by
assigning it the return value of a given function. Observing when a
data property changes value is trivial. The problem becomes: deciding
when the function should be re-evaluated.

Looked at like this, the first thing that becomes obvious is that
there are a class of functions which should never be used: those
functions that can "spontaneously" change value. e.g.

  var i = 1;
  function getVal { return ++i; }
  function getVal2 { return Math.random(); }
  function getVal3 { return myElement.offsetHeight; }

[I actually think at this point it becomes clear that, because the
task at hand isn't solvable in general, it's not appropriate to
include support for it at the language level, but lets continue].

That said, those functions:

-whose output is dependent only on the value of a discrete set of
inputs, (i.e. are stateless)
-don't modify their inputs
-will always return the same output value for the same set of inputs

can be sensibly used for computed property functions. It's worth
noting that even this can be easy to get wrong. For example, many
webdevs might not realize that

  var firstName = 'Rafael';
  var lastName = 'Weinstein';
  function getVal() { return [firstName, lastName]; }

doesn't meet this criteria as its return value is always a different
Array object.

Assuming that you've been careful to pick an appropriate function,
there are two approaches to knowing when to reevaluate it:

1) Dirty-checking: Immediately before each time you "need" the value
of the dependent property.
2) Dependency observation: When one or more of its inputs have changed 
value.

At this point, you have only trade-offs. (1) Is potentially expensive
and hard to do at exactly the right time, but (2) requires having
proper knowledge of the function's inputs.

Obtaining the set of inputs for (2) can be done in two ways:

2a) Require the function author to declare them.
2b) Attempt to discover them by running the function and observing
which inputs it accesses.

(2a) requires some careful attention on the part of the function
author, so in some sense, if (2b) were possible, it would be ideal.
This brings us to what KnockoutJS does and what François proposed, so
let's consider it.

The first problem is what I discussed above, that creating an
*appropriate* function is potentially tricky and/or hard to
understand, and there isn't any way to statically determine if a
function is or is not appropriate.

The second problem is that doing this risks "discovering" inputs that
aren't really inputs at all. In other words, the function, just be
being invoked happens to "touch" a wide swath of objects, even though
they aren't dependencies of the function. This is bad because it would
cause the system to "observe" these objects, which, given modern VMs,
will cause them to "de-optimize" and become slower to access

Thus, offering language-level support for (2b) puts developers in the
risky situations of authoring computed property functions which may

A) not fire their notifications, even though they appear to have "changed 
value"
B) become a "go-slow" button for their entire application

...with no good recourse to discover why either is happening.

Note that François's proposal included a mitigation of (B), in that
you need to whitelist objects as potential discoverable dependencies.
This helps some with the risk of "discovering too many" dependencies,
but it also risks "not discovering enough" dependencies, which becomes
problem (A) again.

----

Thus, what we have is a problem which is really solved through
convention, not through an abstract solution, and thus the most
sensible thing to do is leave it to authors to make trade-offs for
themselves about the pros & cons of the various approaches and the
conventions they imply. 



More information about the es-discuss mailing list