Underscore: Sugestão: _.debounce e _.throttle pegam um parâmetro extra para como combinar argumentos

Criado em 22 set. 2011  ·  11Comentários  ·  Fonte: jashkenas/underscore

Se eu usar _.debounce() para fazer uma função debounce e então chamá-la 3 vezes em sucessão com 3 conjuntos diferentes de argumentos, então (a partir da v1.1.7) a função de carga útil encapsulada será finalmente chamada com os argumentos especificados pelo 3ª chamada - ou seja, o primeiro e o segundo argumentos são descartados.

Embora isso seja geralmente válido (e o que o debounce de chave normalmente faz, portanto, um padrão razoável), eu me vejo querendo usar debounce para acumular argumentos, por exemplo, tenho uma chamada AJAX que pode obter várias chaves ao mesmo tempo, então uso debounce para buffer as teclas por um segundo e, em seguida, emita uma solicitação combinada.

Minha sugestão é, portanto, que o debounce receba um terceiro argumento opcional "combine" que será chamado com 2 argumentos,

  • sendo o 1º a lista de argumentos acumulados até agora (possivelmente indefinido - ou seja, nenhuma lista na primeira chamada)
  • sendo o segundo a lista de argumentos para a última chamada (possivelmente uma lista vazia)
    e retorna a nova lista de argumentos acumulados. Quando a função de carga útil é chamada, a lista acumulada de argumentos é limpa.

Se nenhum valor for passado para o parâmetro combine, a combinação padrão preserva o comportamento existente
function(acc, newargs) { return newargs; }
mas você também pode decidir usar o primeiro conjunto de argumentos
function(acc, newargs) { return acc || newargs; }
ou o que eu quero fazer que é simplesmente anexar todos os argumentos
function(acc,newargs) { return (acc || []).concat(newargs); }
e é claro que outros podem querer fazer algo mais sofisticado

Isso exigiria a seguinte alteração na função de limite interno

  // Internal function used to implement `_.throttle` and `_.debounce`.
  var limit = function(func, wait, debounce, combine) {
    var timeout, allargs;
    return function() {
      var context = this;
      allargs = combine(allargs,  slice.call(arguments,0))
      var throttler = function() {
        timeout = null;
        var args = allargs;
        allargs = undefined;
        func.apply(context, args);
      };
      if (debounce) clearTimeout(timeout);
      if (debounce || !timeout) timeout = setTimeout(throttler, wait);
    };
  };

e, em seguida, uma alteração para debounce para aceitar e passar pelo novo argumento com o valor padrão, se não for especificado.

  _.debounce = function(func, wait, combine) {
    return limit(func, wait, true, combine || function(acc,newargs) { return newargs; });
  };

A função de aceleração correspondente atualmente usa o primeiro conjunto de argumentos sozinho (o acelerador ignora efetivamente as chamadas que ocorrem dentro de milissegundos de espera de uma primeira chamada e usa o primeiro conjunto de argumentos de chamada, debounce efetivamente ignora todas, exceto a última chamada em uma sequência que ocorre dentro do período de espera um do outro), então sugiro o seguinte para preservar novamente o comportamento padrão atual

  _.throttle = function(func, wait, combine) {
    return limit(func, wait, false, combine || function(acc,newargs) { return acc || newargs; });
  };

Essa pareceria a maneira mais fácil e geral de obter essa funcionalidade sem wrappers excessivos para manter as listas de argumentos, mas eu estaria interessado em saber se há uma maneira fácil de conseguir isso sem alterar o sublinhado.

enhancement wontfix

Comentários muito úteis

OK, para não continuar batendo, mas caso alguém esteja olhando para isso algum tempo depois e se perguntando como fazer o mesmo, achei que essa era a maneira mais limpa sem modificar o próprio debounce (eu o adiciono ao objeto _ , outros podem preferir não para)

_.mixin({
  debounceReduce: function(func, wait, combine) {
    var allargs,
        context,
        wrapper = _.debounce(function() {
            var args = allargs;
            allargs = undefined;
            func.apply(context, args);
        }, wait);
        return function() {
            context = this;
            allargs = combine.apply(context, [allargs,  Array.prototype.slice.call(arguments,0)]);
            wrapper();
        };
    }
})

Isso dá uma função debounced que tem seus argumentos reduzidos pela função combine então, por exemplo,

  delayLog = _.debounceReduce(function() { console.log(arguments); }, 5000, 
                              function(acc,args) { return (acc || []).concat(args); });
  delayLog(3,4);
  delayLog(7,8,9);

alguns segundos depois chamará console.log com o array [3,4,7,8,9]

Todos 11 comentários

Comentando minha própria sugestão, a chamada para combine() deve especificar o mesmo contexto que a função de carga útil, portanto

allargs = combine.apply(this, [allargs, slice.call(arguments,0)])

caso os argumentos precisem acessar o objeto de contexto....

Agora deve ser corrigido no mestre. throttle deve exibir o comportamento correto onde ele sempre usa a cópia mais recente de seus argumentos, dispara uma vez imediatamente e a cada N segundos depois ... e se reinicia N segundos depois ocorreu o último gatilho à direita.

Acho que seu comentário próximo se aplica a outro problema (provavelmente #170), pois o problema levantado por essa solicitação ainda se aplica ao mestre.
Ainda não há uma maneira fácil de ter argumentos de debounce ou de acumulação de aceleração das chamadas que estão sendo combinadas, e ainda acho que é uma adição opcional útil que deixa o comportamento padrão inalterado quando o argumento de combinação opcional não é especificado.

Ah, você está certo. Acumular argumentos está fora do escopo do Underscore - sinta-se à vontade para armazenar seus dados acumulados em um bom local externo às funções _.throttle e _.debounce .

É uma pena, considero o debounce como uma espécie de fold-left (reduce) em várias chamadas com tempo limite, portanto, o acumulador ...

OK, para não continuar batendo, mas caso alguém esteja olhando para isso algum tempo depois e se perguntando como fazer o mesmo, achei que essa era a maneira mais limpa sem modificar o próprio debounce (eu o adiciono ao objeto _ , outros podem preferir não para)

_.mixin({
  debounceReduce: function(func, wait, combine) {
    var allargs,
        context,
        wrapper = _.debounce(function() {
            var args = allargs;
            allargs = undefined;
            func.apply(context, args);
        }, wait);
        return function() {
            context = this;
            allargs = combine.apply(context, [allargs,  Array.prototype.slice.call(arguments,0)]);
            wrapper();
        };
    }
})

Isso dá uma função debounced que tem seus argumentos reduzidos pela função combine então, por exemplo,

  delayLog = _.debounceReduce(function() { console.log(arguments); }, 5000, 
                              function(acc,args) { return (acc || []).concat(args); });
  delayLog(3,4);
  delayLog(7,8,9);

alguns segundos depois chamará console.log com o array [3,4,7,8,9]

@schmerg — isso parece tremendamente útil. Você estaria disposto a licenciar esse código sob a licença do MIT? (Um "sim" será suficiente!)

@markjaquith Claro - sim. Seria um prazer...

Se alguém vier e quiser uma versão js moderna atualizada / comentada do acima:

_.mixin({
  debounceReduce(func, wait, combine) {
    let allArgs; // accumulator for args across calls

    // normally-debounced fn that we will call later with the accumulated args
    const wrapper = _.debounce(() => func(allArgs), wait);

    // what we actually return is this function which will really just add the new args to
    // allArgs using the combine fn
    return (...args) => {
      allArgs = combine(allArgs,  [...args]);
      wrapper();
    };
  },
});

@kmannislands Ei, sua versão não redefine allArgs dentro wrapper() , portanto, as chamadas subsequentes para func() obtêm lotes históricos de argumentos, bem como o lote atual.

Não deveria ser:

const wrapper = _.debounce(() => {
    const args = allArgs;
    allArgs = undefined;
    func(args);
}, wait);

@markjaquith +1

Também func( args ) difere da versão original que usa func.apply(context, args); .
Para o primeiro, args é usado como está no destino func() , enquanto no posterior _(código original)_ você precisa usar arguments em uma função normal ou ( ...args ) em uma função de seta gorda es6.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

afranioce picture afranioce  ·  8Comentários

haggholm picture haggholm  ·  8Comentários

Francefire picture Francefire  ·  5Comentários

githublyp picture githublyp  ·  3Comentários

arieljake picture arieljake  ·  4Comentários