Timing Controls - Part 2

In a previous post, we looked at some techniques to control the timings of functions (callbacks), triggered when specific events are fired. The techniques are:

  1. Debounce
  2. Immediate
  3. Throttle

In this post, we will see how those techniques can be implemented as a generic API, in the form of higher order functions.

We will work with the Debounce implementation:

const delta = 1000;
let timeoutID = null;

function foo() {
  console.log('bar');
}

function debouncedFoo() {
  clearTimeout(timeoutID);
  timeoutID = setTimeout(() => {
    foo();
  }, delta);
}

window.onkeydown = debouncedFoo;

There are few problems with this implementation.

1. Passing arguments

First of all, what if foo expects some arguments? We need to provide those arguments on call. This problem can be resolved by adding parameters to debouncedFoo.

function foo(a, b, c) {
  // ...
}

function debouncedFoo(x, y, z) {
  // ...
  foo(x, y, z);
  // ...
}

2. Functions with variable arity

Arity is the number of arguments the function takes. If we know how many arguments foo requires, the above works just fine. In case of variable arity function, we can use the arguments object.

We will use .apply() method because it allows function execution with the arguments being passed as an array or array-like object (ex: arguments).

function foo(a, b, c) {
  // ...
}

function debouncedFoo(...args) {
  foo.apply(null, args);
  // ...
}

3. Higher order function wrappers

Hardcoding timing controls into our callbacks isn’t good. It’s better if the callback and the timing can act separately. We can achieve this by using Higher order functions.

const delta = 1000;

function log(e) {
  console.log(e);
}

function debounce(fn, delta) {
  let timeoutID = null;

  return (...args) => {
    clearTimeout(timeoutID);

    const args = arguments;
    timeoutID = setTimeout(() => {
      fn.apply(null, args);
    }, delta);
  };
}

const debouncedLog = debounce(log, delta);
window.onkeydown = debouncedLog;

4. Preserve context

Up until now, we can convert any number of functions into debounced versions of themselves, along with passing the arguments. But there’s another problem that arises - context loss.

When calling .apply() on a function, we must also pass the proper context for functions that use this internally. Therefore, it must be provided externally.

// ...

function debounce(fn, delta, context) {
  // ...
  fn.apply(context, args);
  // ...
}

Conclusion

Finally, we arrive at the Debounce HOF. Here’s a demo …

See the Pen Timing Controls | Debounce by Shirsh Zibbu (@zhirzh) on CodePen.

… And the code implementations:

Debounce

function debounce(fn, delta, context) {
  let timeoutID = null;

  return (...args) => {
    clearTimeout(timeoutID);

    const args = arguments;
    timeoutID = setTimeout(() => {
      fn.apply(context, args);
    }, delta);
  };
}

Immediate

function immediate(fn, delta, context) {
  let timeoutID = null;
  const safe = true;

  return (...args) => {
    const args = arguments;

    if (safe) {
      fn.call(context, args);
      safe = false;
    }

    clearTimeout(timeoutID);
    timeoutID = setTimeout(() => {
      safe = true;
    }, delta);
  };
}

Throttle

function throttle(fn, delta, context) {
  const safe = true;

  return (...args) => {
    const args = arguments;

    if (safe) {
      fn.call(context, args);

      safe = false;
      setTimeout(() => {
        safe = true;
      }, delta);
    }
  };
}

Here’s a demo of all three techniques:


The End

In the final post, we will see an implementation of Throttle that works well with browsers.

See the Pen Timing Controls by Shirsh Zibbu (@zhirzh) on CodePen.