Smarter JavaScript Timeouts (v2)

24,595 reads

Timers with Cleared, Pending, Executed, & Paused States

Primitives

If you have spent any time at all writing JavaScript, you are likely to be well acquainted with some of the most ancient functionality built into the language, namely setTimeout() and clearTimeout() .

If you somehow are unfamiliar with JS timers, setTimeout() allows you to schedule a delay in milliseconds after which time it will trigger execution of a callback function (or other arbitrary chunk of code, though for the purposes of this article, we will focus on callbacks). It returns a unique timer identifier that allows you the ability to cancel the timeout before it has triggered by passing it to clearTimeout() .

function greetWorld() {

alert('Hello, world!');

}

var timerId = setTimeout(greetWorld, 2000);

This is about as simplistic of an example as possible. Two seconds after the setTimeout() statement is executed, the greetWorld() callback is run.

Before those two seconds have elapsed, you may cancel the pending call by calling clearTimeout(timerId) . Other than setting and clearing a timeout, there is no other useful functionality provided.

Limitations

When you are coding for straightforward requirements, the built-in functionality may suffice, however its limitations can leave much to be desired. Specifically, there are some unanswerable questions you may need answered:

Has a timeout been created for my callback? Is the execution of my timeout’s callback still pending? Has my timeout’s callback been executed yet?

Typically, if a developer needs any of these questions answered in their project, they may write some task-specific utility logic to help achieve that end. My approach was to create a generalized solution that would provide the missing functionality enumerated here, without hindering or complicating the ease of use and features provided by the built-in primitives.

Timeout.js

npm install smart-timeout

The Timeout object is an interactive, stateful interface that seeks to accomplish the goals described above. Using the revealing module pattern, it exposes the following functions:

Timeout.set(callback, delay = 0, param1, param2, ...)

Timeout.set(customId, callback, delay = 0, param1, param2, ...)

Timeout.clear(key, delete = true)

Timeout.exists(key)

Timeout.pending(key)

Timeout.remaining(key)

Timeout.executed(key)

Timeout.pause(key)

Timeout.paused(key)

Timeout.resume(key)

Timeout.restart(key)

When setting a new timeout, you may optionally define a custom string identifier ( customId ) by which to uniquely identify it. If this parameter is omitted, then callback itself will act as the timeout’s unique identifier. In each function described as accepting key as its parameter, key represents either the customId or callback , whichever was used in the corresponding call to Timeout.set() .

Timeout.set() returns a function that when executed returns a boolean indicating whether or not the delay has elapsed and the callback has been triggered. It is equivalent to calling Timeout.executed() . If the same identifier is repeated in a call to Timeout.set() , the former will be cleared before the latter is added. (If you intentionally want to set multiple concurrent timeouts for the same callback, just use a distinct customId for each.)

Timeout.clear() will simply clear the timeout associated with the specified key (if there is such a timeout) and erase any evidence that the timeout ever existed. It returns no value.

Timeout.exists() returns true if a timeout has been set and not cleared for the specified key , regardless of whether or not its delay has elapsed.

Timeout.pending() returns true if a timeout exists for the specified key and its delay has not yet elapsed (i.e., its callback has not yet been triggered).

Timeout.remaining() [added in v2] returns the milliseconds remaining in the countdown until execution.

Timeout.executed() returns true if a timeout exists for the specified key and its delay has elapsed (i.e., its callback has been triggered).

Timeout.pause() [added in v2] allows you to pause the countdown for a timer that has not yet executed.

Timeout.paused() [added in v2] returns true if a pending timeout is currently paused.

Timeout.resume() [added in v2] allows you to resume the countdown for a paused timer.

Timeout.restart() [added in v2] allows you to restart the countdown for a pending or paused timer with the original delay time.

Basic Functionality

Considering the very simplistic example callback provided earlier, the most basic functionality is demonstrated here. (Note that the remainder of the code examples in this article will use ES6.)

const didGreet = Timeout.set(greetWorld, 2000)

if (Timeout.exists(greetWorld)) {// true

console.log('greeting has been scheduled')

}

if (Timeout.pending(greetWorld)) {// true

console.log('greeting is waiting to be issued')

}

// ...wait for 2 seconds to elapse...

if (didGreet()) {// true

console.log('the greeting was issued')

}

// ^that is identical to calling this:

if (Timeout.executed(greetWorld)) {// true

console.log('as I said, the greeting was issued')

}

Timeout.pending(greetWorld) // false - it ran

Timeout.exists(greetWorld) // true - it still exists

Timeout.clear(greetWorld)

Timeout.exists(greetWorld) // false - it has been cleared

Instead of using the callback as the unique key, you may alternately specify a custom identifier:

const didGreet = Timeout.set('myGreeting', greetWorld, 2000)

if (Timeout.exists('myGreeting')) {// true

console.log('greeting has been scheduled')

}

// etc.

See it in Action

Throttling Example

The basic usage by itself is helpful, but let us consider a more complicated use case: throttling excessive window events. In this case, we will not be using a delay to trigger a timeout callback, but merely as a time tracker to test whether or not the specified delay has elapsed.

Let’s say our requirements dictate that we add class is-scrolled to the <html> element whenever the window is scrolled down any distance from the top of the page. The problem is that when the window is scrolling, a flood of scroll events are triggered, and we do not want to bog down the page by reacting to each one. This could be accomplished with a throttle function from an external library like lodash, but for this example, we’ll write it ourselves using Timeout to restrict the onScroll callback so it may execute only periodically.

const throttle =

(delay, callback) =>

(...args) =>

!Timeout.pending(callback) &&

Timeout.set(callback, () => {}, delay)

? callback.apply(this, args)

: null

const onScroll = () => {

const isScrolled = $(window).scrollTop() > 0

$('html').toggleClass('is-scrolled', isScrolled)

}

const onScrollThrottled = throttle(100, onScroll)

$(window).scroll(onScrollThrottled)

The throttle() function accepts as its two parameters the delay in milliseconds and the callback to execute if at least the delay has elapsed since the last time it was executed. It returns a function appropriate for use as an event callback.

This is obviously a very specific (albeit a little convoluted) use of the Timeout object, but it demonstrates the flexibility it provides with succinct, clear operations that are not possible with setTimeout() and clearTimeout() alone.

Conclusion

There may be other libraries out there that provide similar functionality, however, if so they must be well hidden because I have not been able to find them, which is why I decided to write this post. I hope it proves as useful to you as I hope it can be!

Tags