PDA

View Full Version : [JS] TimeoutChainer



Trinithis
11-30-2007, 12:16 AM
1) CODE TITLE: TimeoutChainer

2) AUTHOR NAME/NOTES: Trinithis

3) DESCRIPTION: Replaces for-loop closures to chain window.setTimeout()'s among other things.

4) URL TO CODE: http://trinithis.awardspace.com/TimeoutChainer/TimeoutChainer.js

Demos:
http://trinithis.awardspace.com/TimeoutChainer/demo1.html
http://trinithis.awardspace.com/TimeoutChainer/demo2.html

I made the TimeoutChainer class to avoid excessive closure use when staging a sequence of setTimeout()'s. It is supposed to simplify creating such a task.

-------------------------------

How To:


//Object Arguments

{
callback: null,
context: null,
interval: 0,
delay: -1, //translates to this.interval
numTimes: 1,
args: [],
runAfter: null
}


This is the default object used for TimeoutChainer's object argument. When calling the constructor, you send in one argument that is an object. If that object has a property name that is the same as in the above object, TimeoutChainer will use it's value. If it does not have a corresponding property, then the default object's property value will be used instead.


callback: The function to execute at each timeout. You must supply this argument. It may either be a function reference or a string.

context: An object reference which the callback uses for the this keyword. Primarily will be used for method callbacks. Use null if there is no need.

interval: The time delay between each callback.

delay: When to call the first callback. After that times depend on interval. Negative values get translated into the interval time.

numTimes: The number of times to call the callback function.

args: Holds the arguments callback will use.

runAfter: If an object is specified, will only start the timeouts when that object tells it to start. If the object is a TimeoutChainer (TC), THIS will automatically start its timeouts when TC finishes its timeouts. (delay and interval still come into effect.) Demo1 makes use of this.


Among other things, a variety of properties and methods are also avaliable. It is safe to change most of the supplied "object arguments" whenever you want (... as long as your logic is done correctly). If you change the following properties:


callback: Must be a function reference. Cannot use a string this time.

numTimes, remaining, completed: Once the timeouts stop and you change these, you have to initiate the timeouts again through the chainTimeout() method. Note: Only remaining is actually used to determine whether or not to stop the timeouts, and that happens when it reaches 0.

runAfter: Changing this may cause serious bugs. Leave it alone unless you understand how the constructor uses it.


Methods:

clear(): Clears any existing timeout and will prevent further from being issued until chainTimeout() is called. Sets remaining to 0. Will not change completed. This method will also invoke any TimeoutChainers waiting on this to complete.

chainTimeout(): Will initiate the TimeoutChainer. This will always chain at least one timeout even if remaining equals 0. Invoking this method might cause bugs if you supplied a TimeoutChainer to runAfter in the constructor or if you call it while it is still issuing timeouts. You probably should create a new TimeoutChainer instead of calling this method.


Static Member:

TimeoutChainer.SELF: For the args property of the object arguments, you can use this solitary enum constant. The constructor will translate it into a reference to itself. Only use during the constructor. In addition, there really is no need since you can supply an actual reference after constructed. Demo2 makes use of this static member.


-------------------------------

Example used in demo1:


var list = document.getElementById("list");

list.appendText = function(s) {
var li = document.createElement("li");
li.appendChild(document.createTextNode(s));
list.appendChild(li);
}

var tc = new TimeoutChainer({
callback: "appendText",
args: [{i:1, toString:function(){return String(this.i++)}}],
context: list,
interval: 100,
numTimes: 10
});

new TimeoutChainer({
callback: list.appendText,
args: [{i:10, toString:function(){return String(this.i--)}}],
context: list,
interval: 100,
delay: 100,
numTimes: 10,
runAfter: tc
});



Example used in demo2:


//Just think of TextFormatter as a nice way to print stuff on the screen

var tf1 = new TextFormatter(document.getElementById("div1")).setFontFamily("monospace");
var tf2 = new TextFormatter(document.getElementById("div2")).setFontFamily("monospace");

function write(s, tc, tf) {
tf.println(tc.completed + ":" + s);
}

function clearTC(tc) {
tc.clear();
}

setTimeout(
bundleFunction(
null, clearTC, new TimeoutChainer({
numTimes: 10, interval: 100, startDelay: 200,
callback: write, args: ["hi", TimeoutChainer.SELF, tf1]
})
), 450
);

setTimeout(
bundleFunction(
new TimeoutChainer({
numTimes: 10, interval: 100,
callback: "println", args: [{
i: 1,
toString: function() {
return this.i++ + ":bye";
}
}], context: tf2
}), "clear"
), 550
);

Twey
11-30-2007, 09:52 AM
Handy.

Trinithis
11-30-2007, 07:59 PM
Updated.

Should its completed property increment itself before or after the callback? Currently, it increments after, but I am not sure which would be more useful.

Twey
11-30-2007, 08:14 PM
Before. The user might want to do something in the callback if, e.g., it's been completed five times, and if you increment it after the callback they'd have to check if it's four, not five, which is unintuitive.

One practice I've taken to recently involves using an object to implement named arguments. This is handy because it grants the user a lot more freedom about which arguments to specify. Also, an array of arguments would probably be more useful.

setTimeout2() and setInterval2() aren't semantic names at all. Also, this is already implemented in Firefox. I suggest overriding the defaults where necessary.

Trinithis
11-30-2007, 08:25 PM
Changed the incrementation policy. Same goes for this.remaining decrementation.


Also, this is already implemented in Firefox.
The arguments part is implemented, but you still get errors when using call or apply. Suggestions?

I'm not sure what you mean by objects for named arguments. Perhaps an example?

Twey
11-30-2007, 09:29 PM
How do you mean using call() or apply()? On setTimeout() itself?
function combine() {
for(var r = {}, i = arguments.length - 1, x; i >= 0; --i)
for(x in arguments[i])
if(arguments[i].hasOwnProperty(x))
r[x] = arguments[i][x];

return r;
}

function areaOfRect(args) {
args = combine(args || {}, {
width: 50,
height: 20
});

return args.width * args.height;
}It can then be called:
// 100 * default 20 == 2000
areaOfRect({width: 100});
// default 50 * 400 == 20000
areaOfRect({height: 400});
// 100 * 300 == 30000
areaOfRect({width: 100, height: 300});
// default 50 * default 20 == 1000
areaOfRect();It allows the arguments to be specified in any order, thereby allowing greater flexibility over which arguments are passed and which are left out, as well as being more readable (the arguments are labeled rather than being an apparently-random string of numbers).

Trinithis
11-30-2007, 09:51 PM
Interesting. I really like the idea :D. Will implement.

This is what I mean by call and apply


var o = {
val: "o's value",
display: function() {
alert(this.val);
}
};

setTimeout.call(o, o.display, 100); //error!

//setTimeout2 or whatever it's going to be called

setTimeout2.call(o, o.display, 100); //works

Twey
11-30-2007, 11:43 PM
But why would you ever want to do that? setTimeout() doesn't modify its context, I don't see why you'd want to give it a different context.

Trinithis
12-01-2007, 12:28 AM
var o = {
val: "o's value",
display: function() {
alert(this.val);
}
};

setTimeout(o.display, 100); //"undefined"


I just thought changing the this context was more sugary through the call method rather than through an explicit argument:


setTimeout(o.display, 100, o);

vs.

setTimeout.call(o, o.display, 100);


This might not seem so much sugar, but when mixing up default/custum contexts with variable arguments, I think it does indeed become apparent. For example


setTimeout(document.write, 100, document, "write");
setTimeout(alert, 100, null, "alert");

vs.

setTimeout.call(document, document.write, 100, "write");
setTimeout(alert, 100, "alert"); //no need for anything special here

Maybe it's just me that thinks the latter is more elegant, but others might not think so. In that case, I'll change it.

Your combine is buggy. Perhaps you meant to iterate the loop from the last argument to the first argument? Does defining x outside the for-in make it faster?


function combine() {
for(var r = {}, i = arguments.length-1, x; i >= 0; --i)
for(x in arguments[i])
if(arguments[i].hasOwnProperty(x))
r[x] = arguments[i][x];

return r;
}


BTW, thanks for all your help and input so far.

Twey
12-01-2007, 12:37 AM
Your combine is buggy. Perhaps you meant to iterate the loop from the last argument to the first argument?It is, and I did.
Does defining x outside the for-in make it faster?No, I originally had two for/ins so I put it outside to avoid redeclaration. May as well have all the declarations in one place anyway.
Maybe it's just me that thinks the latter is more elegant, but others might not think so. In that case, I'll change it.What you've done there is an abuse of call(). If a function's context is modified with call(), it's expected that the function itself will do something with that context. However, in your code it's just passed onto the argument.

I suggest a separate function:
function bundleFunction(context, func, args) {
context = context || null;

if(typeof func === "string" && context)
func = context[func];

if(!args)
args = [];
else if(!(args instanceof Array))
args = [args];

return function() {
return func.apply(context, args);
};
}Then:
setTimeout(bundleFunction(document, "write", "some text"), 100);

Trinithis
12-01-2007, 02:53 AM
Update. Hopefully, I got it all right now. If not I'll give it another run :p