PDA

View Full Version : Extending Object with watch()/unwatch()


freedom_razor
03-01-2009, 02:19 AM
Below is the code which extends global Object with watch() and unwatch() methods, which aren't supported by all browsers. I've tested it on IE7 and FF3 [overriding default methods], and it works fine.
The code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title></title>
<style type="text/css">body{font-family:Arial;font-size:15px;}dt{font-size:14px;}dd{color:red;}</style>
</head><body>
<p>Use:<br />object.watchIt(property, handler_function);<br />object.unwatchIt(property);</p>
<p>handler_function has three arguments passed to it: <i>function handler(id, old_value, new_value)</i></p>
<ul><li>property name</li><li>property's value before change</li><li>property's value after change.</li></ul><hr />
<p>Two different objects, both having a property with identical name and one object with two different properties:</p>
<dl><dt>tracking changes for: myObject1.myProperty1</dt><dd id="test">&nbsp;</dd>
<dt>tracking changes for: myObject2.myProperty1</dt><dd id="test2">&nbsp;</dd>
<dt>tracking changes for: myObject2.myProperty2</dt><dd id="test3">&nbsp;</dd></dl>
<p>Examples of unwatchIt():</p>
<dl><dt><a href="" onclick="myObject1.unwatchIt('myProperty1');return false;">Stop tracking myProperty1 of myObject1</a></dt>
<dd>myObject1.myProperty1, now unwatched: <span id="counter"></span></dd>
<dt><a href="" onclick="myObject2.unwatchIt('myProperty1');return false;">Stop tracking myProperty1 of myObject2</a></dt>
<dd>myObject2.myProperty1, now unwatched: <span id="counter2"></span></dd>
<dt><a href="" onclick="myObject2.unwatchIt('myProperty2');return false;">Stop tracking myProperty2 of myObject2</a></dt>
<dd>myObject2.myProperty2, now unwatched: <span id="counter3"></span></dd></dl><hr />

<script type="text/javascript">
//- <![CDATA[
Object.prototype.watchIt = function(p,f) {
if(!Object.watch) {canDo(this, p, f);}
else {this.watch(p,f);}
}
Object.prototype.unwatchIt = function(p) {
if(!Object.unwatch) {unDo(this, p);}
else {this.unwatch(p);}
}
function unDo(o, p) {
eval('clearInterval(o.'+p+'timerID);');
}
function canDo(o, p, f){
var state=o[p]; var control=function(){if(o[p]!=state){state=f(p,state,o[p])};};
eval('o.'+p+'timerID = setInterval(control, 100);');
}
//-------example objects-----------------------------------------
var myObject1={myProperty1:0}
var myObject2={myProperty1:0, myProperty2:555};
//-------using watchIt to track changes in properties-----
myObject1.watchIt("myProperty1", testIt);
myObject2.watchIt("myProperty1", testIt2);
myObject2.watchIt("myProperty2", testIt3);
//-------example handler functions----------------------------
function testIt(id, oldval, newval) {
document.getElementById('test').innerHTML=id+' was: '+oldval+', now is: '+newval;
return newval; //important if you want to keep track of the value before change
};
function testIt2(id, oldval, newval) {
document.getElementById('test2').innerHTML=id+' was: '+oldval+', now is: '+newval;
return newval; //important if you want to keep track of the value before change
};
function testIt3(id, oldval, newval) {
document.getElementById('test3').innerHTML=id+' was: '+oldval+', now is: '+newval;
return newval; //important if you want to keep track of the value before change
};
//-------code to change properties' values-------------------
setInterval(letsGo, 1000);
function letsGo(){
myObject1.myProperty1++; myObject2.myProperty1--; myObject2.myProperty2--;
document.getElementById('counter').innerHTML=' myProperty1: ' + myObject1.myProperty1;
document.getElementById('counter2').innerHTML=' myProperty1: ' + myObject2.myProperty1;
document.getElementById('counter3').innerHTML=' myProperty2: ' + myObject2.myProperty2;
}
//- ]]>
</script>
</body>
</html>

How it works?
It adds watchIt() and unwatchIt() methods to Object prototype. If watch() or unwatch() is already supported, the internal code is used. My code only applies when Object is missing those methods.
Use:
- o.watchIt(p,f) where o is your object, p is property you want to track, and f is a function executed on change of p
- o.unwatchIt(p) removes tracking of changes from p

Code above is full HTML page with examples of use. Feel free to copy&paste and see how it works. If you only want the code, it's in red.
Many thanks to Gjslick for his insight and input.

Any feedback welcome.

Gjslick
03-01-2009, 07:26 PM
Hey Freedom. There ya go, thought you'd need to poll it with an interval. Your only problem is now, that you are using global variables (state and control) which are fine for that one object / property, but will continually overwrite themselves and get mixed up when using them for multiple objects and properties, causing the code to continually think that each of the properties have changed.

You'll be able to see what I'm talking about if you try to track the number of changes that the code thought each property took when using two properties. Works perfectly in FF with the built-in watch methods, which correctly show that there were changes to both properties, once every second (a total of 2 changes each second).

In IE on the other hand, because those two variables are being overwritten by the next watch (namely, the state variable), when watching the two properties, the code is comparing the first property's state to the second one's actual value, and then vice-versa. This causes the code to think that each property has changed every time a poll is made for a change (every 10th of a second), when in reality, each one is only changed once every full second. Try the code below in IE. I added an output for the number of times the code thinks that a value has changed.


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title></title>
</head>
<body>

<div id="test">testing...</div>
<div id="test2">testing2...</div>

TestIt Calls: <span id="testItCalls"></span>&nbsp;(the number of times the code thinks that either value was changed)<br><br>


<a href="" onclick="po.unwatchIt('exa'); return false;">unwatch() #1</a><br>
<a href="" onclick="po.unwatchIt('exa2'); return false;">unwatch() #2</a><br>

<div>Still going, #1 unwatched: <span id="counter">.</span></div>
<div>Still going, #2 unwatched: <span id="counter2">.</span></div>

<script type="text/javascript">
//- <![CDATA[

Object.prototype.watchIt = function(p,f) {
if(!Object.watch) {new canDo(this, p, f);}
else {this.watch(p,f);}
}

Object.prototype.unwatchIt = function(p) {
if(!Object.unwatch) {new unDo(this, p);}
else {this.unwatch(p);}
}

function unDo(o, p) {
this.obj = o; this.prop = p;
eval('clearInterval('+p+'timer);');
}

function canDo(o, p, f){
this.obj = o; this.prop = p; this.func = f;
state=this.obj[this.prop];
control=function(){if(o[p]!=state){state=f(p,state,o[p])};};
eval(p+'timer = setInterval(control, 100);');
}

//-------example object and example function--------------------------------

var po={ exa:0, exa2: 0 };

var testItCalls = 0;
po.watchIt("exa", testIt);
po.watchIt("exa2", testIt2);

function testIt(id, oldval, newval) {
testItCalls++;
document.getElementById( 'testItCalls' ).innerHTML = testItCalls;

document.getElementById('test').innerHTML='Registered change: '+id+' was: '+oldval+', now is: '+newval;
return newval; //important if you want to keep track of the value before change
};

function testIt2(id, oldval, newval) {
testItCalls++;
document.getElementById( 'testItCalls' ).innerHTML = testItCalls;

document.getElementById('test2').innerHTML='Registered change: '+id+' was: '+oldval+', now is: '+newval;
return newval; //important if you want to keep track of the value before change
};

setInterval(letsGo, 1000);
function letsGo(){
po.exa++; // incremement property1
po.exa2--; // decrement property2
document.getElementById('counter').innerHTML=' exa: ' + po.exa;
document.getElementById('counter2').innerHTML=' exa: ' + po.exa2;
}
//- ]]>
</script>

</body>
</html>You'll also see that the oldVal's are getting mixed up as well. Now, if you try unwatching one of the variables, the behavior returns back to normal, because a second watched variable is no longer using those same state and control variables.

A possible fix for this would be to maintain a separate state variable for each object and property combination.

Gjslick
03-01-2009, 09:52 PM
Alright, after posting I just had to try to figure it out, lol. This would be an update to your canDo function. I create the 'state' global variable as an object that will be indexed by object and property, which will store the state (oldVal) for each property of the object independently.


var state = new Object(); // Global variable, available to all functions. Will be indexed by object and property (o and p) to store old values
function canDo( o, p, f ){
this.obj = o;
this.prop = p;
this.func = f;

if( !state[o] ) // If state has not yet been indexed by o, create an object to store its properties
state[o] = new Object();

state[o][p] = this.obj[this.prop]; // Store the state of this object's property in the global 'state' object, which is indexed by object and property
control = function() { // Create a function to compare the old value and its current value
if( o[p] != state[o][p] ) {
state[o][p] = f( p, state[o][p], o[p] )
}
}
eval( p+'timer = setInterval( control, 100 );' );
}You'll still have a problem with how you named that timer (if say, two objects have the same property name), but this solves the problem of watching more than one property. All the best.

-Greg

freedom_razor
03-01-2009, 11:20 PM
Thanks for that, you're right it didn't work well. But as for the solutions, maybe it is simpler to just change the scope to local with var. Seems to work ok now.

var state=this.obj[this.prop];
var control=function(){if(o[p]!=state){state=f(p,state,o[p])};};


Yeah, I need to think about unwatch() more. This dynamic timer naming was the quick and dirty solution I came up with, already being aware it may lead to problems. I'll do testing and adjust the code this week [hopefully] :)

Gjslick
03-02-2009, 12:27 AM
Freedom, good call on that! Making those variables local works perfectly. The state variable is captured by the control function, and then the control function is set to run on the next interval. Then state and control go out of scope (but are still maintained internally by the reference created by setInterval), and the next call to canDo() uses brand new variables! Beautiful!

In fact, the entire function can now be reduced to the following, and doesn't even need to be called as a contructor anymore (i.e. no need for the 'new' keyword in front of the function call):

function canDo( o, p, f ){
var state = o[p];
var control = function() { // Create a function to compare the old value and its current value
if( o[p] != state )
state = f( p, state, o[p] )
}
eval( p+'timer = setInterval( control, 100 );' );
}Really the only thing left to figure out is what to do with the timers. Nice job tho! :thumbsup:

freedom_razor
03-02-2009, 06:39 PM
Looks like the only thing to do with those timers was to make them a property of the object, using tracked property's name in constructing that new property for timers.
I've done some testing, the code in first post is a complete HTML page with tests if you want to have a look.

Gjslick
03-03-2009, 04:56 AM
Yeah, that was a good idea making the timer a property in the object. Might just want to name it with a few underscores or something on the off chance that there is a watched property named 'myProperty' as well as another user property named 'myPropertytimerID' :p But good job man.

The only thing to note to everyone is that with the nature of the 100 millisecond interval between checks to properties (and even if we put it down to 1 millisecond), this shouldn't be used to run a function where subsequent statements rely on the outcome of that function. Since that might be a tad confusing, here's an example:

var o = {
a : 1,
b : 1
}

// Function to increment the property 'b' by 1 (for when 'a' is changed)
function increment_b() {
o.b++;
}

// Watch 'a', and increment 'b' when its changed
o.watchIt( 'a', increment_b );

// Change 'a'
o.a++; // o.a is changed to 2. We would expect o.b to be incremented to 2 as well (from the watch function)
alert( o.b ); // This alert shows us '1' in IE, and '2' in Firefox
The reason the alert shows us '1' in IE is because IE has not yet run the code to check for a change in the property by the time it spawns that alert box. IE has to wait for the timer interval to complete before it can compare the old value and the new value, and then it can run the increment_b() function. By that time, the alert window has already been spawned with the original value of o.b.

Conversely, Firefox's built-in watch() method causes the increment_b() function to be run immediately when o.a is changed (just as if 'increment_b();' was in-line with the code), and the alert shows us exactly what we would expect and desire.

If only IE would just implement the watch() method in its JavaScript interpreter (which is really the only place where a true watch method can be implemented), then the world of JavaScript would be a better place!! :p

This will be wonderful for debugging tho, and I plan on adding your script on my site for just such purposes! Again, great job! :thumbsup:

(And btw, thanks for the shout out! :D)

freedom_razor
03-03-2009, 03:32 PM
The only thing to note to everyone is that with the nature of the 100 millisecond interval between checks to properties (and even if we put it down to 1 millisecond), this shouldn't be used to run a function where subsequent statements rely on the outcome of that function. ...*snip*
The reason the alert shows us '1' in IE is because IE has not yet run the code to check for a change in the property by the time it spawns that alert box. IE has to wait for the timer interval to complete before it can compare the old value and the new value, and then it can run the increment_b() function. By that time, the alert window has already been spawned with the original value of o.b.


Hmm, not sure it's about timers, a couple of brackets () when calling the handler function solves that problem ;)
o.watchIt('a', increment_b());

Gjslick
03-03-2009, 07:55 PM
Lol, Freedom, that only seemed to work because by putting the parenthesis in there, you actually called the increment_b() function instead of passing it into watchIt() as a reference!

So the alert will give '2' (because increment_b() actually ran), but you'll notice that o.a is not actually watched, and you'll also notice that wonderfully descriptive IE error message of "Object Expected" :p (in FF, you get "f is not a function").

This is because having the parenthesis in there causes the increment_b() function to run, and then the return value of the function is passed into watchIt (which is undefined), instead of an actual reference to the function.

freedom_razor
03-03-2009, 11:28 PM
Damn, you're right again :) You could get around it with timeout, if you really need to:
var o={a:1, b:1}
function increment_b(id, oldval, newval) {
o.b++;
return newval; // need that to stop o.b incrementing
}
o.watchIt('a', increment_b);
o.a++
setTimeout('alert(o.b)',100)

At the moment I can't see any other way of working it out, maybe tomorrow when I catch some free time, though I doubt I can solve it.
Gotta love IE... [it is not going to work on IE DOM objects, because they aren't exactly Javascript objects - well, you can rewrite it using Prototype or similar framework that has functions to wrap IE DOM]

Gjslick
03-04-2009, 08:50 PM
Haha, yeah... Unfortunately, you'd have to write your whole application around timeouts, lol.

But also, timeouts aren't exactly an exact science. What happens is when you create a timeout or an interval, the browser creates a new thread of processing for the responding function and schedules it to run at at that time. However, with the inherently inconsistent nature of multiprocessing and multithreading, the browser's scheduler actually runs that responding function approximately at that time.

This could mean that that the browser may get it right and run the watch interval at exactly 100ms intervals each time (like we want), or it might only get around to it at 105ms, 120ms, or even longer depending on the situation. This could also mean that even though we set our second timeout to run that alert box 100ms later (as to run after the watch interval), the alert box could still possibly run before increment_b() depending on the internal scheduling situation anyway! (Thus foiling our plot :p)

But as a side note, I remember looking into that watch method probably 10 years ago (or more...) in a JavaScript 1.2 book, back when Netscape Navigator 4 was the latest and greatest. I remember reading how Internet Explorer did not support the method, and how it should only be used for debugging purposes while developing in Navigator. Because at that time I was still a newbie (lol), I let it go at that. However, in all those years, IE has still not implemented the watch method, and as such, it's the reason why people use getter and setter methods in their JavaScript objects, instead of public properties.

I do wish that we could write code that could respond to changes in properties (just like built in browser properties such as window.location = "someFile.html"), but I guess it's just not in the cards for JavaScript! What can ya do. :rolleyes:

Happy coding buddy.

-Greg