Debounce a function with argument
Asked Answered
C

4

12

I'm trying to debounce a save function that takes the object to be saved as a parameter for an auto-save that fires on keystroke. The debounce stops the save from happening until the user stops typing, or at least that's the idea. Something like:

var save = _.debounce(function(obj) {
  ... 
}, delay);

Where this falls apart is if I try to save two objects in quick succession. Because the debounce doesn't take the passed in object into account, only the second call to save will fire and only one object will be saved.

save(obj1);
save(obj2);

Will only save obj2, for example.

I could make obj an instance of a class that has its own save method that takes care of debouncing saves to just that object. Or keep a list of partial/curried functions somewhere, but I'm hoping there's a one stop solution out there. Something like:

var save = _.easySolution(function(obj) {
  ... 
}, delay);

What I'm looking for the following string of saves to save each object, but only save each object once.

save(obj1);
save(obj2);
save(obj3);
save(obj2);
save(obj2);
save(obj3);
save(obj2);
save(obj1);

EDIT: Something like this, maybe, just not so convoluted, and something that doesn't mutate the obj with a __save function?

function save(obj) {
  if(!obj.__save) {
    obj.__save = _.debounce(_.partial(function(obj) {
      ...
    }, obj), delay);
  }

  obj.__save();
}
Cannon answered 28/2, 2015 at 21:59 Comment(7)
Couldn't get the question even after reading it twice, but seems like you want .bind()Pedicle
It sounds like you don't actually want to debounce then.Lawman
@Pedicle Tried to clarify.Cannon
@FelixKling It's kind of a debounce. With a debounce, if a ton of saves were fired in quick succession (<delay), then only the last call to save would actually fire. The problem is that only the last save is firing. Meaning in that last example, only obj1 would be saved.Cannon
So you need debounce per object? It's actually looks like a job for RxJS (or any other reactive programming library) with .Distinct() operator combined with debounce. (actually it's not as simple as I just mentioned, but I think it's a direct use of Rx anyway)Pedicle
@Pedicle RxJS looks very cool. I'll read up on that. But overkill for this one problem. What do you think of my proposed solution using underscore/lodash? It works, but is kind of hard to look at.Cannon
It will indeed work. I would move that __save into a dedicated object, instead of modifying obj. If it's possible for you to use WeakMap then that's what I would use/Pedicle
A
30

You're going to want to create a debounced version of the function for each argument that get's passed. You can do this fairly easily using debounce(), memoize(), and wrap():

function save(obj) {
    console.log('saving', obj.name);
}

const saveDebounced = _.wrap(
    _.memoize(() => _.debounce(save), _.property('id')),
    (getMemoizedFunc, obj) => getMemoizedFunc(obj)(obj)
)

saveDebounced({ id: 1, name: 'Jim' });
saveDebounced({ id: 2, name: 'Jane' });
saveDebounced({ id: 1, name: 'James' });
// → saving James
// → saving Jane

You can see that 'Jim' isn't saved because an object with the same ID is saved right after. The saveDebounced() function is broken down as follows.

The call to _memoize() is caching the debounced function based on some resolver function. In this example, we're simply basing it on the id property. So now we have a way to get the debounced version of save() for any given argument. This is the most important part, because debounce() has all kinds of internal state, and so we need a unique instance of this function for any argument that might get passed to save().

We're using wrap() to invoke the cached function (or creating it then caching it if it's the first call), and pass the function our object. The resulting saveDebounced() function has the exact same signature as save(). The difference is that saveDebounced() will debounce calls based on the argument.


Note: if you want to use this in a more generic way, you can use this helper function:

const debounceByParam = (targetFunc, resolver, ...debounceParams) =>
    _.wrap(
        _.memoize(
            () => _.debounce(targetFunc, ...debounceParams),
            resolver
        ),
        (getMemoizedFunc, ...params) =>
            getMemoizedFunc(...params)(...params)
    )

// And use it like so
function save(obj) {
    console.log('saving', obj.name);
}

const saveDebounced = debounceByParam(save, _.property('id'), 100)
Aminta answered 1/3, 2015 at 15:47 Comment(3)
I like memoize, but it's the opposite of what I need. I'd like the LAST call to save in a given timeframe to run, not the first. Then more saves in future to run as well. Imagine save is posting something to a server via an xhr request. The purpose is to keep the server up to date with the most recent changes to obj in a few calls as possible.Cannon
I'm not sure whether I'm doing something different to nicholas, but, for me the saveDebounced was saving just the last object to be passed.. works perfectly.Lacour
@Cannon This answer is doing exactly what you asked. The debounced function is memoized, but the saving action itself is still debounced. By the way, this answer also works with Underscore.Guereza
C
1

You can use internal object in closure for set/get debounced function.

In this example we check if debounced function with this args alredy saved in our memory object while call debounced function. If no - we create it.

const getDebouncedByType = (func, wait, options) => {
  const memory = {};

  return (...args) => {
    // use first argument as a key
    // its possible to use all args as a key - e.g JSON.stringify(args) or hash(args)
    const [type] = args;

    if (typeof memory[searchType] === 'function') {
      return memory[searchType](...args);
    }

    memory[searchType] = debounce(func, wait, { ...options, leading: true }); // leading required for return promise
    return memory[searchType](...args);
  };
}; 

Original gist - https://gist.github.com/nzvtrk/1a444cdf6a86a5a6e6d6a34f0db19065

Corinnecorinth answered 2/12, 2019 at 16:24 Comment(0)
B
0

Maybe something like:

var previouslySeen = {}

var save = _.debounce(function(obj) {
  var key = JSON.stringify(obj);
  if (!previouslySeen[key]) {
     previouslySeen[key] = 1;
  } else {
     ...
  }
}, delay);
Biddy answered 28/2, 2015 at 22:29 Comment(2)
Have you tried it with some arbitrary objects? Hint: It does not work. Have you ever seen code that uses objects as property keys?Lawman
You are right. I modified the code to stringify the key.Locate
T
0

Although I find Adam Boduch's answer relevant, I find the method with lodash very unintelligible. I personally use a "vanilla" version:

const debounce = (fn, delay = 1000) => {
    let timerId = {};
    return function(...args) {
        const context = this;
        const key = JSON.stringify(args);
        
        clearTimeout(timerId[key]);

        timerId[key] = setTimeout(() => {
            delete timerId[key];
            fn.apply(context, args);
        }, delay);
    };
}

// Example of use
const debouncedFunction = debounce((arg) => {
    console.log('Function called with', arg);
}, 2000);

debouncedFunction('param1'); // Will run after 2 seconds
debouncedFunction('param1'); // Cancels the first call, reschedules another call for 2 seconds later
debouncedFunction('param2'); // Will run in parallel with the second call, after 2 seconds

This approach also has the advantage of not depending on the format of the function parameter. Here, the function to be debounced can receive a single parameter (which is not specifically an object), but can also contain several of different types.

Tetherball answered 10/1 at 10:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.