Snippets

Fantom-Factory Debouncing and Throttling Event Handlers

Created by Steve Eynon last modified

Debouncing and Throttling Event Handlers

See http://www.alienfactory.co.uk/articles/debouncing-and-throttling for a live demo.

using dom::Win

** A means to rate-limit function calls.
** 
** To use 'debounce()' and 'throttle()' pass a function in and get a function out, then use the 
** returned function in place of the original. It doesn't matter how frequently the new function 
** is invoked, the original function is only invoked according the debounce / throttle rules.
** 
** See `http://www.alienfactory.co.uk/articles/debouncing-and-throttling` for live demo.
** 
@Js
mixin Debounce {

	** Debounces the execution of a function. Debouncing, unlike throttling,
	** guarantees that a function is only executed a single time, either at the
	** very beginning of a series of calls, or at the very end. If you want to
	** simply rate-limit execution of a function, see the `throttle` method.
	** 
	** In this visualisation, '|' is a debounced-function call and X is the actual
	** callback execution:
	** 
	**   Debounced with `atBegin == false`:
	**   |||||||||||||||||||||||||    (pause)    |||||||||||||||||||||||||
	**                            X                                       X
	**   
	**   Debounced with `atBegin == true`:
	**   |||||||||||||||||||||||||    (pause)    |||||||||||||||||||||||||
	**   X                                       X
	** 
	** Arguments:
	** 
	** 'delay' is a a zero-or-greater delay in milliseconds. For event callbacks, 
	** values around 100 or 250 (or even higher) are most useful.
	** 
	** 'atBegin' if 'false' (default) then callback will only be executed 'delay' 
	** milliseconds after the last debounced-function call.
	** If 'true', the callback will only be executed at the first 
	** debounced-function call.
	** (After the debounced-function has not been called for 'delay' 
	** milliseconds, the internal counter is reset)
	** 
	** Returns a new, debounced, function that should be used in place of the 
	** original.
	** 
	static Func debounce(Duration delay, |Obj?, Obj?, Obj?, Obj?| callback, Bool atBegin := false) {
		_throttle(delay, callback, null, atBegin)
	}
	
	** Throttles the execution of a function. Especially useful for rate limiting
	** execution of handlers on events like resize and scroll. If you want to
	** rate-limit execution of a function to a single time, see the `debounce`
	** method.
	** 
	** In this visualisation, '|' is a throttled-function call and X is the actual
	** callback execution:
	** 
	**	 Throttled with `noTrailing == false`:
	**	 |||||||||||||||||||||||||    (pause)    |||||||||||||||||||||||||
	**	 X    X    X    X    X    X              X    X    X    X    X    X
	**	
	**	 Throttled with `noTrailing == true`:
	**	 |||||||||||||||||||||||||    (pause)    |||||||||||||||||||||||||
	**	 X    X    X    X    X                   X    X    X    X    X
	** 
	** Arguments:
	** 
	** 'delay' is a zero-or-greater delay in milliseconds. For event callbacks, values 
	** around 100 or 250 (or even higher) are most useful.
	** 
	** 'noTrailing' if 'false' (default) then the callback will be executed one final time after the last
	** throttled-function call.
	** If 'true' the callback will only execute while the throttled-function is being called. 
	** (After the debounced-function has not been called for 'delay' milliseconds, 
	** the internal counter is reset)
	** 
	** Returns a new, throttled, function that should be used in place of the 
	** original.
	** 
	static Func throttle(Duration delay, |Obj?, Obj?, Obj?, Obj?| callback, Bool noTrailing := false) {
		_throttle(delay, callback, noTrailing, null)
	}
	
	private static Func _throttle(Duration delay, |Obj?, Obj?, Obj?, Obj?| callback, Bool? noTrailing, Bool? atBegin) {
		throttling := noTrailing != null
		debouncing := atBegin    != null

		// After wrapper has stopped being called, this timeout ensures that
		// 'callback' is executed at the proper times in 'throttle' and 'end'
		// debounce modes.
		timeoutId	:= null as Int
		lastExec	:= -1
		wrapper		:= |Obj? p1, Obj? p2, Obj? p3, Obj? p4| {
			exec := |->| {
				lastExec = Duration.nowTicks
				callback(p1, p2, p3, p4)
			}
			
			// If `atBegin` is true this is used to clear the flag
			// to allow future `callback` executions.
			reset := |->| {
				timeoutId = null
			}
			
			if (debouncing && atBegin && timeoutId == null)
				exec()
			
			if (throttling && lastExec == -1)
				exec()				

			elapsed := lastExec > -1 ? Duration.nowTicks - lastExec : 0
			
			if (timeoutId != null)
				Win.cur.clearTimeout(timeoutId)
			
			if (throttling)
				if (elapsed > delay.ticks)
					exec()
				else if (!noTrailing)
					timeoutId = Win.cur.setTimeout(delay, exec)

			if (debouncing)
				timeoutId = Win.cur.setTimeout(delay, atBegin ? reset : exec)
		}
		
		return wrapper.retype(callback.typeof)
	}
}

Comments (0)