As Luke suggested, there is a method remove in the watcher that should solver leaks problem while the new version should be supported by most recent Opera, Safari, FireFox, and Internet Explorer.
Main updates:
- the watcher is always another object and not the original one
- to safely destroy the watcher use its destroy method and then delete it
----------------------------------------------
We all complain about IE and its non-standard attitude, this time I would like to introduce a non standard feature, as Object.prototype.watch is, for Internet Explorer.
What is watch and why we need it
The watch method allows validation, control, monitoring, over a generic object.
This means that we can control properties assignment with one, or more, callbacks.
var man = {};
man.watch("name", function(propertyName, oldValue, newValue){
// propertyName: name
// oldValue: undefined if it is the first time, the old one otherwise
// newValue: new assigned value
return "Mr " + newValue;
});
man.name = "Andrea";
alert(man.name); // Mr Andrea
watch for Internet Explorer, about my implementation
I used a weird strategy to implement this feature in Internet Explorer and it is based on DOM and its IE feature called onpropertychange.
Accordingly, it was not possible to create an Object.prototype.watch, while it was simple to implement a createWatcher callback.
(function(watch, unwatch){
createWatcher = watch && unwatch ?
// Andrea Giammarchi - Mit Style License
function(Object){
var handlers = [];
return {
destroy:function(){
handlers.forEach(function(prop){
unwatch.call(this, prop);
}, this);
delete handlers;
},
watch:function(prop, handler){
if(-1 === handlers.indexOf(prop))
handlers.push(prop);
watch.call(this, prop, function(prop, prevValue, newValue){
return Object[prop] = handler.call(Object, prop, prevValue, newValue);
});
},
unwatch:function(prop){
var i = handlers.indexOf(prop);
if(-1 !== i){
unwatch.call(this, prop);
handlers.splice(i, 1);
};
}
}
}:(Object.prototype.__defineSetter__?
function(Object){
var handlers = [];
return {
destroy:function(){
handlers.forEach(function(prop){
delete this[prop];
}, this);
delete handlers;
},
watch:function(prop, handler){
if(-1 === handlers.indexOf(prop))
handlers.push(prop);
if(!this.__lookupGetter__(prop))
this.__defineGetter__(prop, function(){return Object[prop]});
this.__defineSetter__(prop, function(newValue){
Object[prop] = handler.call(Object, prop, Object[prop], newValue);
});
},
unwatch:function(prop){
var i = handlers.indexOf(prop);
if(-1 !== i){
delete this[prop];
handlers.splice(i, 1);
};
}
};
}:
function(Object){
function onpropertychange(){
var prop = event.propertyName,
newValue = empty[prop]
prevValue = Object[prop],
handler = handlers[prop];
if(handler)
attachEvent(detachEvent()[prop] = Object[prop] = handler.call(Object, prop, prevValue, newValue));
};
function attachEvent(){empty.attachEvent("onpropertychange", onpropertychange)};
function detachEvent(){empty.detachEvent("onpropertychange", onpropertychange);return empty};
var empty = document.createElement("empty"), handlers = {};
empty.destroy = function(){
detachEvent();
empty.parentNode.removeChild(empty);
empty = handlers = null;
};
empty.watch = function(prop, handler){handlers[prop] = handler};
empty.unwatch = function(prop){delete handlers[prop]};
attachEvent();
return (document.getElementsByTagName("head")[0] || document.documentElement).appendChild(empty);
}
)
;
})(Object.prototype.watch, Object.prototype.unwatch);
A first example, based on precedent one, is this one:
var // original object
man = {},
// create its watcher
manWatcher = createWatcher(man);
manWatcher.watch("name", function(propertyName, oldValue, newValue){
return "Mr " + newValue;
});
// assign name property
manWatcher.name = "Andrea";
// retrieve original object property
alert(man.name); // Mr Andrea
Another example is based on validation, or better, a one shot assignment over the watcher:
var obj = {},
watcher = createWatcher(obj);
watcher.watch("test", function(prop, oldValue, newValue){
return oldValue === undefined ? newValue : oldValue;
});
watcher.test = "one assignment";
watcher.test = "never again";
alert(obj.test); // one assignment
Cool?
About limits
The main one is that the for in loop will not work as expected over a watcher element because it is a DOM node and it will expose every property.
Another one could be about memory leaks and/or low execution, since there are a couple of things to do during assignment.
The good part is that the watcher and the watched will have the same property value, except for those attributes that we cannot modify in a DOM node (style, for example).
Last but not least, happy new year! :D
Interesting! I wonder why the wrapping function passing indexOf, since it's not used in the closure.
ReplyDeleteShame the added DOM nodes won't get cleaned up when the watched object is GCd. Could lead to some serious memory leakage if used in a loop or high traffic util function.
I suppose neither Opera nor Safari have any comparable path?
Luke, the indexOf was for another test I did but I forgot to remove it from the code, thanks.
ReplyDeleteI could not find a way to automatically clean everything on watcher delete and any sugestion will be appreciated.
Opera and Safari, as far as I know, supports __defineSetter__ and __defineGetter__, am I wrong? I could extend the function to those browsers as well ( right now I think Opera could work without problems, but I did not test :) )
Luke, I implemented a destroy method to remove the element and/or callbacks and solve memory leaks problem plus I have implemented an Opera / Safari version.
ReplyDeleteThe script is beta but seems to work properly ;-)
Could you please explain this "watcher" concept again. Also, is your implementation cross-browser compat?
ReplyDeleteAnonymous, this is the MDC explanation of the watch Object prototype: Object watch
ReplyDeletewhile for the other question, yes, the last version is compatible with Opera and Safari and probably others.
doesn't work at all
ReplyDeleteno errors but no result
function myObj()
{
this.towatch=false;
manWatcher=createWatcher(this);
manWatcher.watch("towatch",function(){alert("ok");});
}
var z=new myObj();
z.towatch=true;
Hi Andrea,
ReplyDeleteI'm trying to find a good way to implement an object.watch functionality. The event chain in my application relies on it. I tried the Eli Grey polyfill, but it gave me some issues witch the variables becoming inaccessible. I wondered what your thought at the present time are about the watch function. Do you think your function is still considered a good practice?
wkr,
Jerry
I don't think this post is updated enough so I think it's not a solution at all ... not even sure if still works.
ReplyDeleteHowever, what you are looking for is `Object.observe` in Chrome or `Proxy`, both should land via ES6 soon as JavaScript core functionality.
I hope this answer helped.