Smarter JavaScript Timeouts (v2)
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 ranTimeout.exists(greetWorld) // true - it still existsTimeout.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)
: nullconst 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!
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!