Come controllare i cambiamenti di array?


106

In Javascript, c'è un modo per essere avvisati quando un array viene modificato utilizzando push, pop, shift o assegnazione basata su indice? Voglio qualcosa che accenda un evento che posso gestire.

Conosco la watch()funzionalità di SpiderMonkey, ma funziona solo quando l'intera variabile è impostata su qualcos'altro.

Risposte:


169

Ci sono alcune opzioni ...

1. Ignora il metodo push

Seguendo il percorso veloce e sporco, potresti sovrascrivere il push()metodo per il tuo array 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 In alternativa, se desideri scegliere come target tutti gli array, puoi eseguire l'override Array.prototype.push(). Usa cautela, però; altro codice nel tuo ambiente potrebbe non gradire o aspettarsi quel tipo di modifica. Tuttavia, se un catch-all sembra allettante, sostituiscilo myArraycon Array.prototype.

Questo è solo un metodo e ci sono molti modi per modificare il contenuto dell'array. Probabilmente abbiamo bisogno di qualcosa di più completo ...

2. Creare un array osservabile personalizzato

Invece di sovrascrivere i metodi, potresti creare il tuo array osservabile. Questo particolare copie di attuazione un array in un nuovo array come oggetto e fornisce personalizzato push(), pop(), shift(), unshift(), slice(), e splice()metodi nonché di accesso indici personalizzati (a condizione che la dimensione della matrice viene modificato solo tramite uno dei metodi di cui sopra o la lengthstruttura).

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());

})();

Vedere per riferimento.Object.defineProperty()

Questo ci avvicina ma non è ancora a prova di proiettile ... il che ci porta a:

3. Proxy

I proxy offrono un'altra soluzione ... permettendoti di intercettare le chiamate ai metodi, le funzioni di accesso, ecc. Soprattutto, puoi farlo senza nemmeno fornire un nome di proprietà esplicito ... che ti consentirebbe di testare un accesso arbitrario basato su indice / Incarico. Puoi persino intercettare l'eliminazione della proprietà. I proxy consentirebbero effettivamente di ispezionare una modifica prima di decidere di consentirla ... oltre a gestire la modifica dopo il fatto.

Ecco un esempio ridotto:

(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);

})();


Grazie! Funziona con i metodi array regolari. Qualche idea su come organizzare un evento per qualcosa come "arr [2] =" foo "?
Sridatta Thatipamala,

4
Immagino che potresti implementare un metodo set(index)nel prototipo di Array e fare qualcosa come dice l'antisanità
Pablo Fernandez

8
Sarebbe molto meglio creare una sottoclasse di Array. In genere non è una buona idea modificare il prototipo di Array.
Wayne

1
Eccezionale risposta qui. La classe dell'ObservableArray è eccellente. +1
dooburt

1
"'_array.length === 0 && delete _self [index];" - puoi spiegare questa linea?
splintor

23

Dalla lettura di tutte le risposte qui, ho assemblato una soluzione semplificata che non richiede alcuna libreria esterna.

Illustra anche molto meglio l'idea generale per l'approccio:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};

Questa è una buona idea, ma non pensi che se, ad esempio, voglio implementarlo negli array di dati js del grafico, e ho 50 grafici che significa 50 array e ogni array verrà aggiornato ogni secondo -> immagina la dimensione di l'array 'myEventsQ' alla fine della giornata! Penso che quando è necessario spostarlo di tanto in tanto
Yahya

2
Non capisci la soluzione. myEventsQ È l'array (uno dei tuoi 50 array). Questo frammento non modifica la dimensione dell'array e non aggiunge alcun array aggiuntivo, cambia solo il prototipo di quelli esistenti.
Sych,

1
mmmm vedo, avrebbero dovuto essere fornite ulteriori spiegazioni!
Yahya

3
pushrestituisce il lengthdella matrice. Quindi, puoi ottenere il valore restituito da Array.prototype.push.applyuna variabile e restituirlo dalla pushfunzione personalizzata .
adiga

12

Ho trovato quanto segue che sembra ottenere questo risultato: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays estende il carattere di sottolineatura e può essere utilizzato come segue: (da quella pagina)

// 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');
});

13
Questo è fantastico, ma c'è un avvertimento importante: quando un array viene modificato come arr[2] = "foo", la notifica di modifica è asincrona . Poiché JS non fornisce alcun modo per controllare tali modifiche, questa libreria si basa su un timeout che viene eseguito ogni 250 ms e verifica se l'array è cambiato del tutto, quindi non riceverai una notifica di modifica fino al prossimo l'ora in cui scade il timeout. push()Tuttavia, altre modifiche come ricevere una notifica immediata (in modo sincrono).
peterflynn

6
Inoltre immagino che un intervallo di 250 influenzerà le prestazioni del tuo sito se l'array è grande.
Tomáš Zato - Ripristina Monica il

Ho appena usato questo, funziona come un fascino. Per i nostri amici basati sui nodi ho usato questo incantesimo con una promessa. (Il formato nei commenti è un dolore ...) _ = require ('lodash'); require ("underscore-osserva") ( ); Promise = require ("bluebird"); return new Promise (funzione (risoluzione, rifiuto) {return _.observe (coda, 'cancella', funzione () {if ( .isEmpty (coda)) {ritorno risoluzione (azione);}});});
Leif

5

Ho usato il codice seguente per ascoltare le modifiche a un array.

/* @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;
                 }
    });
}

Spero sia stato utile :)


5

La soluzione del metodo push Override più votata da @canon ha alcuni effetti collaterali che erano scomodi nel mio caso:

  • Rende il descrittore della proprietà push diverso ( writablee configurabledovrebbe essere impostato al trueposto di false), il che causa eccezioni in un punto successivo.

  • Solleva l'evento più volte quando push()viene chiamato una volta con più argomenti (come myArray.push("a", "b")), che nel mio caso era inutile e dannoso per le prestazioni.

Quindi questa è la migliore soluzione che ho trovato per risolvere i problemi precedenti ed è a mio parere più pulita / più semplice / più facile da capire.

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
    }
});

Si prega di vedere i commenti per le mie fonti e per suggerimenti su come implementare le altre funzioni mutanti oltre a push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.


@canon Ho Proxy disponibili, ma non posso usarli perché l'array è modificato esternamente, e non riesco a pensare a un modo per forzare i chiamanti esterni (che oltre a cambiare di volta in volta senza il mio controllo) a utilizzare un Proxy .
cprcrack

@canon ea proposito, il tuo commento mi ha fatto fare un'ipotesi sbagliata, che è che sto usando l'operatore spread, quando in realtà non lo sono. Quindi no, non sto sfruttando affatto l'operatore dello spread. Quello che sto usando è il parametro rest che ha una ...sintassi simile e che può essere facilmente sostituito con l'uso della argumentsparola chiave.
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);

1
Sembra Object.observe()e Array.observe()sono stati ritirati dalle specifiche. Il supporto è già stato ritirato da Chrome. : /
canonico

0

Non sono sicuro che questo copra assolutamente tutto, ma uso qualcosa di simile (specialmente durante il debug) per rilevare quando un array ha un elemento aggiunto:

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;
    }
});


-1

Ho giocherellato e ho pensato a questo. L'idea è che l'oggetto abbia tutti i metodi Array.prototype definiti, ma li esegua su un oggetto array separato. Questo dà la possibilità di osservare metodi come shift (), pop () ecc. Anche se alcuni metodi come concat () non restituiscono l'oggetto OArray. Il sovraccarico di questi metodi non renderà l'oggetto osservabile se vengono utilizzate le funzioni di accesso. Per ottenere quest'ultimo, le funzioni di accesso sono definite per ogni indice entro una data capacità.

Per quanto riguarda le prestazioni ... OArray è circa 10-25 volte più lento rispetto al semplice oggetto Array. Per la capacità in un intervallo 1 - 100 la differenza è 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;
    }
}

Sebbene funzioni su elementi esistenti, non funziona quando un elemento viene aggiunto con array [new_index] = value. Solo i proxy possono farlo.
mpm

-5

Non ti consiglierei di estendere i prototipi nativi. Invece, puoi usare una libreria come new-list; https://github.com/azer/new-list

Crea un array JavaScript nativo e ti consente di iscriverti a qualsiasi modifica. Rende gli aggiornamenti in batch e ti fornisce il diff finale;

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]

})
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.