A JavaScript Function Guard

Here’s a utility that marshals your function requests so the function only gets invoked after you’ve finally stopped requesting it. For example if I push a button and then wait for a given interval, the function gets called, but if I push it again before the interval has elapsed, the clock is reset and I have to wait another 500ms.

There are several practical applications for such a feature: preventing processing of user input until the user has finished typing; only registering the scrolling event when the scrolling has finished; waiting for resizing to complete before recalculating dimensions. These behaviors are proxies for the imaginary events onKeyEnd, onScrollEnd and onResizeEnd respectively which suggests the typical use case pattern.

Here’s the most basic usage example (thanks to Nick Fitzgerald for pointing out my beginner’s error ;-) ):

var resizeMonitor = new FunctionGuard(resized);
window.onresize =  function() {resizeMonitor.run()}

This will execute the resized function 500ms (the default interval) after the user has finished resizing the window (note that throughout these examples I’m using a naive approach to event handling due to the lack of cross-browser compatability and a reluctance to introduce framework code unless it’s directly relevant to the discussion):

Let’s take a look at the implementation. You’ll notice some additional, optional parameters which I’ll demonstrate shortly:

function FunctionGuard(fn, quietTime, context /*,fixed args*/) {
	this.fn = fn;
    this.quietTime = quietTime || 500;
	this.context = context || null;
    this.fixedArgs = (arguments.length > 3) ? Array.prototype.slice.call(arguments, 3) : [];
}

FunctionGuard.prototype.run = function(/*dynamic args*/) {
    this.cancel(); //clear timer
    var fn = this.fn, context = this.context, args = this.mergeArgs(arguments);
    var invoke = function() {
    	fn.apply(context,args);
    }
    this.timer = setTimeout(invoke,this.quietTime); //reset timer
}

FunctionGuard.prototype.mergeArgs = function(dynamicArgs) {
    return this.fixedArgs.concat(Array.prototype.slice.call(dynamicArgs,0)); 
}

FunctionGuard.prototype.cancel = function(){
    this.timer && clearTimeout(this.timer);
}

The FunctionGuard constructor has only one required parameter, the function to be invoked. You can optionally specify the desired quiet time (defaults to 500ms), the this context of the invocation and any number of fixed arguments to be pre-assigned to all invocations in the style of curry

The run method is used to request an invocation of the target function, which has the effect of resetting the timer. Invocation will only actually occur when this method has not been called for a period equal to the specified quiet time. You can use run to pass any number of additional arguments to the target function which be concatenated with any fixed arguments already defined in the constructor.

The other two methods are used internally. Note that mergeArgs concatenates the dynamic and fixed arguments and that this function gets invoked every time run is called, regardless of whether the resultant concatenation will ever be used. This is somewhat inefficient but I wasn’t prepared to sacrifice code clarity for a minuscule performance bump. So there!

Here’s a couple of simple examples you can test in your console:

//simple test
var logWhenDone = new FunctionGuard(console.log);
//typo...
logWhenDone.run('testnig');
//within 500ms correct to...
logWhenDone.run('testing'); //console logs -> 'testing'
//set a fixed param and a time
var logWhenDone = new FunctionGuard(console.log, 5000, null, 'hello');
a.run('don't log this');
//within 5 seconds add...
a.run('you can log this now'); //console logs -> 'hello you can log this now'

And here’s a more practical use case that makes use of all the optional arguments. This acts like a sort of auto-twitter (but without the network which makes it pretty lame ;-)). Enter a message and 2 seconds after you stop typing the message gets logged in your console twitter style.

Reference the code from an HTML file or run it standalone from the console and look for a new input control at the bottom of your current page. It should work on Firefox, Chrome, Safari and IE8+ (Chrome and Safari look less pretty because their consoles’ public APIs do not support the console.clear function).

//Requires FunctionGuard utility. 

if (typeof console == "undefined") {alert("show your console and refresh");}

var messageManager = {
	history: [],

	logMessages: function() {
		console.clear ? console.clear() : console.log('----------------------');
		for (var i=0; i<this.history.length; i++) {
			var message = this.history[i];
			var secondsAgo = Math.round(((+new Date) - message.time)/1000);
			console.log(message.text + ' (' + secondsAgo + ' seconds ago via ' + message.via.id + ')');
		}
	},

	addMessage: function(element, text) {
		element.value = '(message logged)';
		element.select();
		var message = {
			text: text,
			time: +new Date,
			via: element
		}
		this.history.push(message);
		this.logMessages();
	}
}


var messager = document.createElement('INPUT');
messager.setAttribute('id','inputter');
messager.setAttribute('value','what are you doing?');
messager.setAttribute('size',70);
document.body.appendChild(messager);
messager.select();
var messageMonitor = new FunctionGuard(messageManager.addMessage, 2000, messageManager, messager);
messager.onkeyup = function() {messageMonitor.run(messager.value)};
About these ads

7 thoughts on “A JavaScript Function Guard

    • Hi Nick

      Yes thanks for catching that – beginners javascript error ;-), and yep event handler binding is always good practice – although if resized does not use this we’re ok.

      Going to look at your blog post now

      • Well actually, you resize *is* using this because the run method refers to this.timer, etc, so it breaks without binding resizeMonitor to this.

        Love the new design you have here, btw.

      • Yep sorry – I was assuming the approach I used in final example – put the event handler in an anon function (and use closure for args if necessary)

        window.onresize = function() {resizeMonitor.run()}

  1. Cool! I always ran into the same problem (especially with running some logic once scroll or resize event had finished). So, I did something similar focused on bind/trigger with jQuery in a plugin called bindWithDelay.

    * Demo (scroll down to the bottom of the page to see the # of times each event was called both natively and “guarded”): http://briangrinstead.com/files/bindWithDelay/
    * Code: http://github.com/bgrins/bindWithDelay/blob/master/bindWithDelay.js

  2. This reminds me of similar problem when a user clicks the button again before previous request had actually finished, we can go do something like this to be on safer path:

    // simple way to avoid running code again when user click a button or trigger event in some other way

    var shutdown = function() {
    shutdown = function() {
    alert(“Already processing”);
    }

    alert(‘This runs first!’);

    }

    shutdown();
    shutdown();

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s