Como observar as mudanças na matriz?

106

Em Javascript, existe uma maneira de ser notificado quando uma matriz é modificada usando push, pop, shift ou atribuição baseada em índice? Quero algo que acione um evento com o qual eu possa lidar.

Eu sei sobre a watch()funcionalidade do SpiderMonkey, mas isso só funciona quando a variável inteira está definida para outra coisa.

Sridatta Thatipamala
fonte

Respostas:

169

Existem algumas opções...

1. Substitua o método push

Seguindo o caminho rápido e sujo, você pode substituir o push()método para sua matriz 1 :

Object.defineProperty(myArray, "push", {
  enumerable: false, // hide from for...in
  configurable: false, // prevent further meddling...
  writable: false, // see above ^
  value: function () {
    for (var i = 0, n = this.length, l = arguments.length; i < l; i++, n++) {          
      RaiseMyEvent(this, n, this[n] = arguments[i]); // assign/raise your event
    }
    return n;
  }
});

1 Alternativamente, se desejar direcionar todos os arrays, você pode substituir Array.prototype.push(). No entanto, tenha cuidado; outro código em seu ambiente pode não gostar ou esperar esse tipo de modificação. Ainda assim, se um pega-tudo soar atraente, apenas substitua myArraypor Array.prototype.

Agora, esse é apenas um método e existem muitas maneiras de alterar o conteúdo do array. Provavelmente precisamos de algo mais abrangente ...

2. Crie uma matriz observável personalizada

Em vez de substituir métodos, você pode criar seu próprio array observável. Neste particular cópias de execução uma matriz para uma nova matriz como objecto e fornece costume push(), pop(), shift(), unshift(), slice(), e splice()métodos , bem como assessores índice personalizado (desde que o tamanho da matriz só é modificada através de um dos métodos acima referidos ou a lengthpropriedade).

function ObservableArray(items) {
  var _self = this,
    _array = [],
    _handlers = {
      itemadded: [],
      itemremoved: [],
      itemset: []
    };

  function defineIndexProperty(index) {
    if (!(index in _self)) {
      Object.defineProperty(_self, index, {
        configurable: true,
        enumerable: true,
        get: function() {
          return _array[index];
        },
        set: function(v) {
          _array[index] = v;
          raiseEvent({
            type: "itemset",
            index: index,
            item: v
          });
        }
      });
    }
  }

  function raiseEvent(event) {
    _handlers[event.type].forEach(function(h) {
      h.call(_self, event);
    });
  }

  Object.defineProperty(_self, "addEventListener", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function(eventName, handler) {
      eventName = ("" + eventName).toLowerCase();
      if (!(eventName in _handlers)) throw new Error("Invalid event name.");
      if (typeof handler !== "function") throw new Error("Invalid handler.");
      _handlers[eventName].push(handler);
    }
  });

  Object.defineProperty(_self, "removeEventListener", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function(eventName, handler) {
      eventName = ("" + eventName).toLowerCase();
      if (!(eventName in _handlers)) throw new Error("Invalid event name.");
      if (typeof handler !== "function") throw new Error("Invalid handler.");
      var h = _handlers[eventName];
      var ln = h.length;
      while (--ln >= 0) {
        if (h[ln] === handler) {
          h.splice(ln, 1);
        }
      }
    }
  });

  Object.defineProperty(_self, "push", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function() {
      var index;
      for (var i = 0, ln = arguments.length; i < ln; i++) {
        index = _array.length;
        _array.push(arguments[i]);
        defineIndexProperty(index);
        raiseEvent({
          type: "itemadded",
          index: index,
          item: arguments[i]
        });
      }
      return _array.length;
    }
  });

  Object.defineProperty(_self, "pop", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function() {
      if (_array.length > -1) {
        var index = _array.length - 1,
          item = _array.pop();
        delete _self[index];
        raiseEvent({
          type: "itemremoved",
          index: index,
          item: item
        });
        return item;
      }
    }
  });

  Object.defineProperty(_self, "unshift", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function() {
      for (var i = 0, ln = arguments.length; i < ln; i++) {
        _array.splice(i, 0, arguments[i]);
        defineIndexProperty(_array.length - 1);
        raiseEvent({
          type: "itemadded",
          index: i,
          item: arguments[i]
        });
      }
      for (; i < _array.length; i++) {
        raiseEvent({
          type: "itemset",
          index: i,
          item: _array[i]
        });
      }
      return _array.length;
    }
  });

  Object.defineProperty(_self, "shift", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function() {
      if (_array.length > -1) {
        var item = _array.shift();
        delete _self[_array.length];
        raiseEvent({
          type: "itemremoved",
          index: 0,
          item: item
        });
        return item;
      }
    }
  });

  Object.defineProperty(_self, "splice", {
    configurable: false,
    enumerable: false,
    writable: false,
    value: function(index, howMany /*, element1, element2, ... */ ) {
      var removed = [],
          item,
          pos;

      index = index == null ? 0 : index < 0 ? _array.length + index : index;

      howMany = howMany == null ? _array.length - index : howMany > 0 ? howMany : 0;

      while (howMany--) {
        item = _array.splice(index, 1)[0];
        removed.push(item);
        delete _self[_array.length];
        raiseEvent({
          type: "itemremoved",
          index: index + removed.length - 1,
          item: item
        });
      }

      for (var i = 2, ln = arguments.length; i < ln; i++) {
        _array.splice(index, 0, arguments[i]);
        defineIndexProperty(_array.length - 1);
        raiseEvent({
          type: "itemadded",
          index: index,
          item: arguments[i]
        });
        index++;
      }

      return removed;
    }
  });

  Object.defineProperty(_self, "length", {
    configurable: false,
    enumerable: false,
    get: function() {
      return _array.length;
    },
    set: function(value) {
      var n = Number(value);
      var length = _array.length;
      if (n % 1 === 0 && n >= 0) {        
        if (n < length) {
          _self.splice(n);
        } else if (n > length) {
          _self.push.apply(_self, new Array(n - length));
        }
      } else {
        throw new RangeError("Invalid array length");
      }
      _array.length = n;
      return value;
    }
  });

  Object.getOwnPropertyNames(Array.prototype).forEach(function(name) {
    if (!(name in _self)) {
      Object.defineProperty(_self, name, {
        configurable: false,
        enumerable: false,
        writable: false,
        value: Array.prototype[name]
      });
    }
  });

  if (items instanceof Array) {
    _self.push.apply(_self, items);
  }
}

(function testing() {

  var x = new ObservableArray(["a", "b", "c", "d"]);

  console.log("original array: %o", x.slice());

  x.addEventListener("itemadded", function(e) {
    console.log("Added %o at index %d.", e.item, e.index);
  });

  x.addEventListener("itemset", function(e) {
    console.log("Set index %d to %o.", e.index, e.item);
  });

  x.addEventListener("itemremoved", function(e) {
    console.log("Removed %o at index %d.", e.item, e.index);
  });
 
  console.log("popping and unshifting...");
  x.unshift(x.pop());

  console.log("updated array: %o", x.slice());

  console.log("reversing array...");
  console.log("updated array: %o", x.reverse().slice());

  console.log("splicing...");
  x.splice(1, 2, "x");
  console.log("setting index 2...");
  x[2] = "foo";

  console.log("setting length to 10...");
  x.length = 10;
  console.log("updated array: %o", x.slice());

  console.log("setting length to 2...");
  x.length = 2;

  console.log("extracting first element via shift()");
  x.shift();

  console.log("updated array: %o", x.slice());

})();

Veja para referência.Object.defineProperty()

Isso nos aproxima, mas ainda não é à prova de balas ... o que nos leva a:

3. Proxies

Os proxies oferecem outra solução ... permitindo que você intercepte chamadas de métodos, acessadores, etc. Mais importante, você pode fazer isso sem nem mesmo fornecer um nome de propriedade explícito ... o que permitiria que você teste um acesso arbitrário baseado em índice / tarefa. Você pode até interceptar a exclusão de propriedade. Os proxies efetivamente permitiriam que você inspecionasse uma alteração antes de decidir permiti-la ... além de lidar com a alteração após o fato.

Aqui está uma amostra simplificada:

(function() {

  if (!("Proxy" in window)) {
    console.warn("Your browser doesn't support Proxies.");
    return;
  }

  // our backing array
  var array = ["a", "b", "c", "d"];

  // a proxy for our array
  var proxy = new Proxy(array, {
    apply: function(target, thisArg, argumentsList) {
      return thisArg[target].apply(this, argumentList);
    },
    deleteProperty: function(target, property) {
      console.log("Deleted %s", property);
      return true;
    },
    set: function(target, property, value, receiver) {      
      target[property] = value;
      console.log("Set %s to %o", property, value);
      return true;
    }
  });

  console.log("Set a specific index..");
  proxy[0] = "x";

  console.log("Add via push()...");
  proxy.push("z");

  console.log("Add/remove via splice()...");
  proxy.splice(1, 3, "y");

  console.log("Current state of array: %o", array);

})();

cânone
fonte
Obrigado! Isso funciona para os métodos de array regulares. Alguma ideia de como criar um evento para algo como "arr [2] =" foo "?
Sridatta Thatipamala
4
Eu acho que você poderia implementar um método set(index)no protótipo do Array e fazer algo como o anti-sanidade diz
Pablo Fernandez
8
Seria muito melhor criar uma subclasse de Array. Geralmente não é uma boa ideia modificar o protótipo do Array.
Wayne,
1
Excelente resposta aqui. A classe do ObservableArray é excelente. +1
dooburt
1
"'_array.length === 0 && delete _self [index];" - você pode explicar esta linha?
splintor
23

Ao ler todas as respostas aqui, reuni uma solução simplificada que não requer nenhuma biblioteca externa.

Também ilustra muito melhor a ideia geral da abordagem:

function processQ() {
   // ... this will be called on each .push
}

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
Sych
fonte
Esta é uma boa ideia, mas você não acha que se, por exemplo, eu quiser implementar isso em matrizes de dados do gráfico js, ​​e tenho 50 gráficos, o que significa 50 matrizes e cada matriz será atualizada a cada segundo -> imagine o tamanho de o array 'myEventsQ' no final do dia! Acho que quando preciso mudar de vez em quando
Yahya de
2
Você não entende a solução. myEventsQ É o array (um de seus 50 arrays). Este trecho não altera o tamanho do array e não adiciona nenhum array adicional, ele apenas altera o protótipo dos existentes.
Sych
1
mmmm Entendo, mais explicação deveria ter sido fornecida!
Yahya,
3
pushretorna o lengthda matriz. Portanto, você pode obter o valor retornado por Array.prototype.push.applypara uma variável e retorná-lo da pushfunção personalizada .
adiga
12

Encontrei o seguinte que parece cumprir isso: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays estende o sublinhado e pode ser usado da seguinte maneira: (a partir dessa página)

// For example, take any array:
var a = ['zero', 'one', 'two', 'trhee'];

// Add a generic observer function to that array:
_.observe(a, function() {
    alert('something happened');
});
user1029744
fonte
13
Isso é ótimo, mas há uma advertência importante: quando um array é modificado como arr[2] = "foo", a notificação de mudança é assíncrona . Uma vez que JS não fornece nenhuma maneira de observar tais mudanças, esta biblioteca depende de um tempo limite que é executado a cada 250 ms e verifica se o array mudou de alguma forma - então você não receberá uma notificação de mudança até o próximo tempo em que o tempo limite é executado. Outras mudanças, como push()ser notificado imediatamente (de forma síncrona), no entanto.
peterflynn
6
Além disso, acho que o intervalo de 250 afetará o desempenho do seu site se a matriz for grande.
Tomáš Zato - Reintegrar Monica em
Acabei de usar isso, funciona como um encanto. Para nossos amigos baseados em nó eu usei este encantamento com uma promessa. (Formatar comentários é uma dor ...) _ = require ('lodash'); require ("sublinhado-observe") ( ); Promessa = requer ("bluebird"); retornar nova Promessa (função (resolver, rejeitar) {retornar _.observe (fila, 'excluir', função () {if ( .isEmpty (fila)) {retornar resolver (ação);}});});
Leif
5

Usei o código a seguir para ouvir as alterações em uma matriz.

/* @arr array you want to listen to
   @callback function that will be called on any change inside array
 */
function listenChangesinArray(arr,callback){
     // Add more methods here if you want to listen to them
    ['pop','push','reverse','shift','unshift','splice','sort'].forEach((m)=>{
        arr[m] = function(){
                     var res = Array.prototype[m].apply(arr, arguments);  // call normal behaviour
                     callback.apply(arr, arguments);  // finally call the callback supplied
                     return res;
                 }
    });
}

Espero que tenha sido útil :)

Nadir Laskar
fonte
5

A solução de método push Override mais votada por @canon tem alguns efeitos colaterais que eram inconvenientes no meu caso:

  • Isso torna o descritor da propriedade push diferente ( writablee configurabledeve ser definido em truevez de false), o que causa exceções em um ponto posterior.

  • Ele gera o evento várias vezes quando push()é chamado uma vez com vários argumentos (como myArray.push("a", "b")), o que no meu caso era desnecessário e ruim para o desempenho.

Portanto, esta é a melhor solução que encontrei para corrigir os problemas anteriores e é na minha opinião mais limpa / simples / fácil de entender.

Object.defineProperty(myArray, "push", {
    configurable: true,
    enumerable: false,
    writable: true, // Previous values based on Object.getOwnPropertyDescriptor(Array.prototype, "push")
    value: function (...args)
    {
        let result = Array.prototype.push.apply(this, args); // Original push() implementation based on https://github.com/vuejs/vue/blob/f2b476d4f4f685d84b4957e6c805740597945cde/src/core/observer/array.js and https://github.com/vuejs/vue/blob/daed1e73557d57df244ad8d46c9afff7208c9a2d/src/core/util/lang.js

        RaiseMyEvent();

        return result; // Original push() implementation
    }
});

Por favor, veja os comentários das minhas fontes e dicas sobre como implementar as outras funções mutantes além de push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.

cprcrack
fonte
@canon Eu tenho Proxies disponíveis, mas não posso usá-los porque o array é modificado externamente, e não consigo pensar em nenhuma maneira de forçar os chamadores externos (que além de mudarem de tempos em tempos sem meu controle) a usar um Proxy .
cprcrack
@canon e por falar nisso, seu comentário me fez fazer uma suposição errada, que é que estou usando o operador spread, quando na verdade não estou. Então, não, não estou aproveitando o operador de spread de forma alguma. O que estou usando é o parâmetro rest, que tem uma ...sintaxe semelhante e que pode ser facilmente substituído com o uso da argumentspalavra - chave.
cprcrack
0
if (!Array.prototype.forEach)
{
    Object.defineProperty(Array.prototype, 'forEach',
    {
        enumerable: false,
        value: function(callback)
        {
            for(var index = 0; index != this.length; index++) { callback(this[index], index, this); }
        }
    });
}

if(Object.observe)
{
    Object.defineProperty(Array.prototype, 'Observe',
    {
        set: function(callback)
        {
            Object.observe(this, function(changes)
            {
                changes.forEach(function(change)
                {
                    if(change.type == 'update') { callback(); }
                });
            });
        }
    });
}
else
{
    Object.defineProperties(Array.prototype,
    { 
        onchange: { enumerable: false, writable: true, value: function() { } },
        Observe:
        {
            set: function(callback)
            {
                Object.defineProperty(this, 'onchange', { enumerable: false, writable: true, value: callback }); 
            }
        }
    });

    var names = ['push', 'pop', 'reverse', 'shift', 'unshift'];
    names.forEach(function(name)
    {
        if(!(name in Array.prototype)) { return; }
        var pointer = Array.prototype[name];
        Array.prototype[name] = function()
        {
            pointer.apply(this, arguments); 
            this.onchange();
        }
    });
}

var a = [1, 2, 3];
a.Observe = function() { console.log("Array changed!"); };
a.push(8);
Martin Wantke
fonte
1
Parece Object.observe()e Array.observe()foi retirado da especificação. O suporte já foi retirado do Chrome. : /
cânone
0

Não tenho certeza se isso cobre absolutamente tudo, mas eu uso algo assim (especialmente durante a depuração) para detectar quando uma matriz tem um elemento adicionado:

var array = [1,2,3,4];
array = new Proxy(array, {
    set: function(target, key, value) {
        if (Number.isInteger(Number(key)) || key === 'length') {
            debugger; //or other code
        }
        target[key] = value;
        return true;
    }
});
user3337629
fonte
-1

Uma biblioteca de coleção interessante é https://github.com/mgesmundo/smart-collection . Permite que você observe arrays e também adicione visualizações a eles. Não tenho certeza sobre o desempenho, pois estou testando sozinho. Irá atualizar esta postagem em breve.

kontinuidade
fonte
-1

Eu brinquei e descobri isso. A ideia é que o objeto tenha todos os métodos Array.prototype definidos, mas os execute em um objeto array separado. Isso dá a capacidade de observar métodos como shift (), pop () etc. Embora alguns métodos como concat () não retornem o objeto OArray. Sobrecarregar esses métodos não tornará o objeto observável se acessores forem usados. Para alcançar o último, os acessores são definidos para cada índice dentro de uma determinada capacidade.

Em termos de desempenho ... OArray é cerca de 10-25 vezes mais lento em comparação com o objeto Array simples. Para a capacidade em um intervalo de 1 a 100, a diferença é 1x-3x.

class OArray {
    constructor(capacity, observer) {

        var Obj = {};
        var Ref = []; // reference object to hold values and apply array methods

        if (!observer) observer = function noop() {};

        var propertyDescriptors = Object.getOwnPropertyDescriptors(Array.prototype);

        Object.keys(propertyDescriptors).forEach(function(property) {
            // the property will be binded to Obj, but applied on Ref!

            var descriptor = propertyDescriptors[property];
            var attributes = {
                configurable: descriptor.configurable,
                enumerable: descriptor.enumerable,
                writable: descriptor.writable,
                value: function() {
                    observer.call({});
                    return descriptor.value.apply(Ref, arguments);
                }
            };
            // exception to length
            if (property === 'length') {
                delete attributes.value;
                delete attributes.writable;
                attributes.get = function() {
                    return Ref.length
                };
                attributes.set = function(length) {
                    Ref.length = length;
                };
            }

            Object.defineProperty(Obj, property, attributes);
        });

        var indexerProperties = {};
        for (var k = 0; k < capacity; k++) {

            indexerProperties[k] = {
                configurable: true,
                get: (function() {
                    var _i = k;
                    return function() {
                        return Ref[_i];
                    }
                })(),
                set: (function() {
                    var _i = k;
                    return function(value) {
                        Ref[_i] = value;
                        observer.call({});
                        return true;
                    }
                })()
            };
        }
        Object.defineProperties(Obj, indexerProperties);

        return Obj;
    }
}
sysaxis
fonte
Embora funcione em elementos existentes, não funciona quando um elemento é adicionado com array [new_index] = value. Apenas proxies podem fazer isso.
mpm
-5

Eu não recomendaria estender protótipos nativos. Em vez disso, você pode usar uma biblioteca como new-list; https://github.com/azer/new-list

Ele cria uma matriz JavaScript nativa e permite que você assine qualquer alteração. Ele agrupa as atualizações e fornece a diferença final;

List = require('new-list')
todo = List('Buy milk', 'Take shower')

todo.pop()
todo.push('Cook Dinner')
todo.splice(0, 1, 'Buy Milk And Bread')

todo.subscribe(function(update){ // or todo.subscribe.once

  update.add
  // => { 0: 'Buy Milk And Bread', 1: 'Cook Dinner' }

  update.remove
  // => [0, 1]

})
Azer
fonte