Everything you think you know about data binding — and every trick MVC libraries are using to pull it off — is about to be flipped on its head.
At the last ECMA/TC39 Face to Face, Rafael Weinstein presented the latest revision of the Object.observe spec, a work in progress that he and several other members of TC39 are currently authoring. Object.observe is under discussion in TC39 and will hopefully be in a future edition of the ECMAScript 262 standard. To test drive it today, grab this special build of Chromium.
The proposed behaviour of Object.observe is to provide a mechanism to observe and notify programs of mutations to specified JavaScript objects.
- Notify when any property is added.
- Notify when the value of a data property is updated
- Notify when any property is deleted.
- Notify when any property is reconfigured (aspects of a property descriptor are modified, eg. by
Object.freeze
)
Note: by default, get/set accessors won’t notify, but you can create custom notifiers, which I will show you below.
Object.observe
will grant observability only for objects that are explicitly specified in a program—like this:
simple-observe.js
function log( change ) {
// Note that |change.object| is actually a reference to the
// target object.
console.log( "What Changed? ", change.name );
console.log( "How did it change? ", change.type );
console.log( "What is the present value? ", change.object[ change.name ] );
}
var o = {};
// Specify |o| as an observable object
Object.observe(o, function( changes ) {
changes.forEach( log );
});
o.who = "Rick";
/*
What Changed? who
How did it change? new
What is the present value? Rick
*/
o.who = "Rose";
/*
What Changed? who
How did it change? updated
What is the present value? Rose
*/
delete o.who;
/*
What Changed? who
How did it change? deleted
What is the present value? undefined
*/
While that example shines with simplicity, there is more! We can also retrieve a notifier object, with Object.getNotifier
and call its notify()
method with a change record; this allows you to create your own accessor notifiers—take a look:
complex-observe.js
function log( change ) {
// Note that |change.object| is actually a reference to the
// target object.
if ( change.type === "read" ) {
console.log( "What was accessed? ", change.name );
console.log( "What value was given? ", change.oldValue );
}
else {
// Only access properties when the event is not "read",
// Otherwise it will result in an infinite access loop
console.log( "What Changed? ", change.name );
console.log( "How did it change? ", change.type );
console.log( "What was the old value? ", change.oldValue );
console.log( "What is the present value? ", change.object[ change.name ] );
}
}
(function( exports ) {
var priv = new WeakMap();
function User( name ) {
this.name = name;
// Store some sensitive tracking information out of reach
priv.set( this, {
login: Date.now(),
lastSeen: Date.now()
});
// Create an accessor set that will act as an intermediary
// for updating sensitive data bound to this instance
Object.defineProperties( this, {
seen: {
set: function( val ) {
var notifier = Object.getNotifier( this ),
p = priv.get( this );
// The program must trigger an "updated" notification
// manually, since Object.observe will not notify
// for accessors
notifier.notify({
type: "updated",
name: "seen",
oldValue: p.lastSeen
});
// Update and store the sensitive data
p.lastSeen = val;
priv.set( this, p );
},
get: function() {
var notifier = Object.getNotifier( this ),
p = priv.get( this );
// A program can also notify for custom behaviours;
// in this case, theprogram will notify any time
// the property is read.
// WARNING!!!!
// This will fire _every_ _time_ _any_ code
// accesses the .seen property
notifier.notify({
type: "read",
name: "seen",
oldValue: p.lastSeen
});
return p.lastSeen;
}
}
});
// Make all instances of User observable
Object.observe( this, function( changes ) {
console.log( "Observed..." );
changes.forEach( log );
});
}
exports.User = User;
}( this ));
var user = new User("Rick");
console.log( user.seen );
/*
Observed...
What was accessed? seen
What value was given? 1345831387279
*/
user.seen = Date.now();
/*
Observed...
What Changed? seen
How did it change? updated
What was the old value? 1345831387279
What is the present value? 1345831401342
Observed...
// This is triggered by the log() function
// when it displays the present value, in a new turn
What was accessed? seen
What value was given? 1345831401342
*/
console.log( user.seen );
/*
Observed...
What was accessed? seen
What value was given? 1345831401342
*/
user.seen = Date.now();
/*
Observed...
What Changed? seen
How did it change? updated
What was the old value? 1345831401342
What is the present value? 1345831474108
Observed...
// This is triggered by the log() function
// when it displays the present value, in a new turn
What was accessed? seen
What value was given? 1345831474108
*/
Object.freeze( user );
/*
Observed...
What Changed? name
How did it change? reconfigured
What was the old value? Rick
What is the present value? Rick
*/
user.seen = Date.now();
/*
TypeError when trying to call the notifier.
Once an object is frozen, Object.getNotifier()
returns null;
*/
Changes are delivered asynchronously, at the end of every “turn”. Notice the the last value in the change set is the present value…
delivery-1.js
user = new User("Rick");
user.name = "Rose";
user.name = "Alli";
user.name = "Taco";
/*
Observed...
What Changed? name
How did it change? updated
What was the old value? Rick
What is the present value? Taco
What Changed? name
How did it change? updated
What was the old value? Rose
What is the present value? Taco
What Changed? name
How did it change? updated
What was the old value? Alli
What is the present value? Taco
*/
Notice only one “Observed…” was logged in the last example? Same turn changes are delivered together, “sometime in the future” (for explanation of how this model works, take a look at the DOM Mutation Observer spec ):
delivery-2.js
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ].forEach(function( n ) {
user.name = n;
});
/*
Observed...
What Changed? name
How did it change? updated
What was the old value? 10
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 0
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 1
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 2
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 3
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 4
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 5
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 6
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 7
What is the present value? 9
What Changed? name
How did it change? updated
What was the old value? 8
What is the present value? 9
*/
In the output, you’ll see that the “present value” is the value that we expect it to be when the [...].forEach(...)
call is complete–that’s the end of the turn.
When the program requires earlier delivery or any sort of immediate control over delivery, Object.deliverChangeRecords(callback)
will force an immediate delivery if there are pending changeRecords:
delivery-3.js
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ].forEach(function( n ) {
if ( n % 3 === 0 ) {
console.log( "Force delivery @", n );
Object.deliverChangeRecords(log);
}
user.name = n;
});
/*
Force delivery @ 0
Observed...
What Changed? name
How did it change? updated
What was the old value? 9
What is the present value? 2
What Changed? name
How did it change? updated
What was the old value? 0
What is the present value? 2
What Changed? name
How did it change? updated
What was the old value? 1
What is the present value? 2
Force delivery @ 3
Observed...
What Changed? name
How did it change? updated
What was the old value? 2
What is the present value? 5
What Changed? name
How did it change? updated
What was the old value? 3
What is the present value? 5
What Changed? name
How did it change? updated
What was the old value? 4
What is the present value? 5
Force delivery @ 6
Observed...
What Changed? name
How did it change? updated
What was the old value? 5
What is the present value? 8
What Changed? name
How did it change? updated
What was the old value? 6
What is the present value? 8
What Changed? name
How did it change? updated
What was the old value? 7
What is the present value? 8
Force delivery @ 9
Observed...
What Changed? name
How did it change? updated
What was the old value? 8
What is the present value? 9
*/
Data Binding in Web Applications
The rationale behind Object.observe
is to provide a native, highly optimized implementation on which data binding strategies can be built. Modern web applications are generally built using one of these—primarily for some degree of data-binding, along with additional goodies to simplify the workload. Object.observe
is not a drop in replacement by any means, however it does spell the end for atrocious workarounds that include property polling and DOM event “hook-binding” (latching onto every key event fired on a document is insane). In a not-too-distant future, data-binding libs will built on top of smart, asynchronous, natively optimized object observation patterns with Object.observe
at the core.
Uncle JS Wants You
If you’re the author of a data-binding library, I’d like to take this opportunity to challenge you to re-write your code base using Object.observe
as the core of your data observation strategy. The idea is to maintain your current API, but drop whatever mechanism you’re currently using and replace it with one built on top of Object.observe
. Post links to your repo branches in the comments.
A special build of Chromium that has a prototype implementation of Object.observe
is available develop your code and run tests against.
In the meantime, I’ve put together an example data-binding API called Fact that you can use as a guide for getting started on your own re-writes. All of the feedback you provide will be used by ECMA/TC39 to guide the development of the spec.
Special thanks to Rafael Weinstein, Rebecca Murphey, Mike Pennisi and Irene Ros for reviewing this blog post.
[0] ECMA/TC39[1] Face to Face[2] Rafael Weinstein[3] Object.observe spec[4] ES5.1[5] ECMAScript 262, 5.1[6] TODO MVC[7] Fact[8] Chromium
Comments
We moved off of Disqus for data privacy and consent concerns, and are currently searching for a new commenting tool.
Yeah but if you’re not able to automatically detect dependencies like in Knouckout JS, I don’t see how I would use it. I think Proxies are probably better at this than a normal JS API but maybe this has use cases…
I think this tool will be leveraged mostly by people working at the library level, not in ‘user-space’, per se. Frameworks will be able to achieve more performant binding and event support with much less code. For example, YUI already offers the Attribute API which is powerful, but relies on nearly 1,000 LOC of plumbing. I have to re-read the code, but I believe you could cut that down by a sizable margin with observability.
Not to mention this is going to be amazing for debugging in unstable/ill-structured codebases where one’s object properties are never safe from unknown hands. 🙂
YUI Attributes: http://yuilibrary.com/yui/d…
I hadn’t thought about the debugging use case – thanks for mentioning!
You can’t replace YUI attributes here because observed changes are not reported immediately but in batch, at the end of the turn. So if you need to validate the \”written\” data, you need a getter/setter anyway.
Object.watch from Netscape allowed this. But it was worse than getter/setters in terms of optimizability while providing the same level of features.
Ah, you’re right. I guess it could be valuable as a less feature-rich option if you wanted to avoid loading YUI+Y.Attribute or depending on a framework at all. My mistake.
Nice! Good to see this coming down the pipe.
@FremyCompany Well this might be (a bit) faster than Proxies…
Not a browser-implemented Proxy, at least. Proxies do not need to be implemented in JS, browsers could make some of them too.
A simpler API is possible:
http://fremycompany.com/BG/…
Why cant we have a generic function that can observe primitive data types as well? I mean, if I want to observe a single string/number, I would have to wrap it in a object!
Primitives are immutable. How can you change a 5 to be something else? It’s always going to be a 5.
I’m guessing you really want to observe the contents of a variable???
Yes of course, the contents of the variable. It would help in debugging.
One possible deficit is the lack of `newValue` in the change record. Sure, you can reconstruct what it was by traversing the array of change records, but why not just include it? That seems like it would be beneficial to any code that needs to reconstruct an audit trail — and easy to implement, too.
When I was whining about this on twitter last week, I hadn’t seen
Object.deliverChangeRecords(cb). That seems like it could help in situations in which you need change records to be acted upon in the current execution path (\”sync\”).
Hm, but judging from the last code snippet, Object.deliverChangeRecords isn’t sync, it’s just \”sooner\”. Why is this useful?
Hi, it could be interesting to add a third argument to filter a particular kind of mutation to observe : read, new, modify. I should be better for performances.
Initial attempt at a poly; https://gist.github.com/417…
I admit I need to find a proper way to catch new/deletes and update the property definer to use existing getters and setters when the property descriptor says they exist. Getting tired though, will look more at it tomorrow.
This also starts to work with your sample code above showing similar (useable) results for updates. I’ve seen other starting points, but none that are trying to stay with the naming/constructs in the proposal. Not perfect, but it’s a start.
knockout.js, just saying, but this is cool too. Going to play around with it over the weekend, been using knockout for two years+ and created tiraggo.js based on it, will probably create tiraggo2.js based on the new built in observable stuff, looks great, excited. I think the observable stuff is built into chrome and you can turn it on in the advanced settings via experimental javascript or some such flag
Nice article Rick! I was also playing around with the API some time ago and found that the nature of Observe is driven by the event loop very annoying. For me it would be much better if this would have been done in the main thread and brings various problems. I’ve written a blog post about this. Would be great to hear your thoughts on this.
https://mindtheshift.wordpr…
Aurelia.js has taken up the challenge from Uncle JS.