I have been studying JavaScript frameworks recently, such as Angular and Meteor, and I was wondering how they would know when the property of an object changed so that they could update the DOM.
I was a little surprised that Angular used plain old JS objects instead of requiring you to call some kind of getter / setter so that it could connect and make the necessary updates. I understand that they just regularly check objects for changes.
But with the advent of getters and setters in JS 1.8.5, we can do better, right?
As a little proof of concept, I put together this script:
( Edit: updated code to add support for dependent properties / methods)
function dependentProperty(callback, deps) { callback.__dependencies__ = deps; return callback; } var person = { firstName: 'Ryan', lastName: 'Gosling', fullName: dependentProperty(function() { return person.firstName + ' ' + person.lastName; }, ['firstName','lastName']) }; function observable(obj) { if (!obj.__properties__) Object.defineProperty(obj, '__properties__', { __proto__: null, configurable: false, enumerable: false, value: {}, writable: false }); for (var prop in obj) { if (obj.hasOwnProperty(prop)) { if(!obj.__properties__[prop]) obj.__properties__[prop] = { value: null, dependents: {}, listeners: [] }; if(obj[prop].__dependencies__) { for(var i=0; i<obj[prop].__dependencies__.length; ++i) { obj.__properties__[obj[prop].__dependencies__[i]].dependents[prop] = true; } delete obj[prop].__dependencies__; } obj.__properties__[prop].value = obj[prop]; delete obj[prop]; (function (prop) { Object.defineProperty(obj, prop, { get: function () { return obj.__properties__[prop].value; }, set: function (newValue) { var oldValue = obj.__properties__[prop].value; if(oldValue !== newValue) { var oldDepValues = {}; for(var dep in obj.__properties__[prop].dependents) { if(obj.__properties__[prop].dependents.hasOwnProperty(dep)) { oldDepValues[dep] = obj.__properties__[dep].value(); } } obj.__properties__[prop].value = newValue; for(var i=0; i<obj.__properties__[prop].listeners.length; ++i) { obj.__properties__[prop].listeners[i](oldValue, newValue); } for(dep in obj.__properties__[prop].dependents) { if(obj.__properties__[prop].dependents.hasOwnProperty(dep)) { var newDepValue = obj.__properties__[dep].value(); for(i=0; i<obj.__properties__[dep].listeners.length; ++i) { obj.__properties__[dep].listeners[i](oldDepValues[dep], newDepValue); } } } } } }); })(prop); } } return obj; } function listen(obj, prop, callback) { if(!obj.__properties__) throw 'object is not observable'; obj.__properties__[prop].listeners.push(callback); } observable(person); listen(person, 'fullName', function(oldValue, newValue) { console.log('Name changed from "'+oldValue+'" to "'+newValue+'"'); }); person.lastName = 'Reynolds';
Which magazines:
Title changed from "Ryan Gosling" to "Ryan Reynolds"
The only problem I see is to define methods like fullName() for the person object, which will depend on two other properties. This requires a small additional add-in on the object so that developers can specify the dependency.
Other than this, are there any flaws in this approach?
Jsfiddle