Wie auf Array-Änderungen achten?

106

Gibt es in Javascript eine Möglichkeit, benachrichtigt zu werden, wenn ein Array durch Push-, Pop-, Shift- oder indexbasierte Zuweisung geändert wird? Ich möchte etwas, das ein Ereignis auslöst, mit dem ich umgehen kann.

Ich kenne die watch()Funktionalität in SpiderMonkey, aber das funktioniert nur, wenn die gesamte Variable auf etwas anderes gesetzt ist.

Sridatta Thatipamala
quelle

Antworten:

169

Es gibt einige Möglichkeiten ...

1. Überschreiben Sie die Push-Methode

Wenn Sie den schnellen und schmutzigen Weg gehen, können Sie die push()Methode für Ihr Array 1 überschreiben :

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 Alternativ können Sie überschreiben , wenn Sie alle Arrays als Ziel festlegen möchten Array.prototype.push(). Seien Sie jedoch vorsichtig; Anderer Code in Ihrer Umgebung mag oder erwartet diese Art von Änderung möglicherweise nicht. Dennoch ersetzen , wenn ein catch-all - Klänge ansprechend, nur myArraymit Array.prototype.

Dies ist nur eine Methode, und es gibt viele Möglichkeiten, den Array-Inhalt zu ändern. Wir brauchen wahrscheinlich etwas umfassenderes ...

2. Erstellen Sie ein benutzerdefiniertes beobachtbares Array

Anstatt Methoden zu überschreiben, können Sie Ihr eigenes beobachtbares Array erstellen. Diese besondere Implementierung Kopien ein Array in eine neue Array-ähnliches Objekt und benutzerdefinierte liefert push(), pop(), shift(), unshift(), slice(), und splice()Methoden sowie kundenspezifische Index Accessoren (vorausgesetzt , dass die Array - Größe nur über eine der zuvor genannten Verfahren oder modifiziert wird lengthEigenschaft).

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

})();

Sehen Object.defineProperty() als Referenz.

Das bringt uns näher, aber es ist immer noch nicht kugelsicher ... was uns zu Folgendem bringt:

3. Proxies

Proxies bieten eine andere Lösung ... mit der Sie Methodenaufrufe, Accessoren usw. abfangen können. Am wichtigsten ist, dass Sie dies tun können, ohne einen expliziten Eigenschaftsnamen anzugeben ... mit dem Sie nach einem beliebigen, indexbasierten Zugriff suchen können. Zuordnung. Sie können sogar das Löschen von Eigenschaften abfangen. Mit Proxies können Sie eine Änderung effektiv überprüfen, bevor Sie sich dafür entscheiden, sie zuzulassen ... zusätzlich zur nachträglichen Bearbeitung der Änderung.

Hier ist ein reduziertes Beispiel:

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

})();

Kanon
quelle
Vielen Dank! Das funktioniert für die regulären Array-Methoden. Irgendwelche Ideen, wie man ein Ereignis für etwas wie "arr [2] =" foo "
auslöst
4
Ich denke, Sie könnten eine Methode set(index)in Arrays Prototyp implementieren und so etwas tun, wie es die Antisanität sagt
Pablo Fernandez,
8
Es wäre viel besser, Array in Unterklassen zu unterteilen. Es ist im Allgemeinen keine gute Idee, den Prototyp von Array zu ändern.
Wayne
1
Hervorragende Antwort hier. Die Klasse des ObservableArray ist ausgezeichnet. +1
Dooburt
1
"'_array.length === 0 && delete _self [index];" - Können Sie diese Zeile erklären?
Schiene
22

Nachdem ich alle Antworten hier gelesen habe, habe ich eine vereinfachte Lösung zusammengestellt, für die keine externen Bibliotheken erforderlich sind.

Es zeigt auch viel besser die allgemeine Idee für den Ansatz:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
Sych
quelle
Das ist eine gute Idee, aber denken Sie nicht, dass ich, wenn ich dies zum Beispiel in Diagramm-js-Daten-Arrays implementieren möchte, 50 Diagramme habe, was bedeutet, dass jedes Array jede Sekunde aktualisiert wird -> stellen Sie sich die Größe von vor das 'myEventsQ'-Array am Ende des Tages! Ich denke, wenn es hin und wieder verschoben werden muss
Yahya
2
Sie verstehen die Lösung nicht. myEventsQ IST das Array (eines Ihrer 50 Arrays). Dieses Snippet ändert die Größe des Arrays nicht und fügt keine zusätzlichen Arrays hinzu. Es ändert nur den Prototyp der vorhandenen Arrays.
Sych
1
mmmm Ich verstehe, aber es hätte mehr Erklärung geben sollen!
Yahya
3
pushGibt das lengthdes Arrays zurück. Sie können also den von Array.prototype.push.applyeiner Variablen zurückgegebenen Wert abrufen und von der benutzerdefinierten pushFunktion zurückgeben.
Adiga
12

Ich habe Folgendes gefunden, was dies zu erreichen scheint: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays erweitert den Unterstrich und kann wie folgt verwendet werden: (von dieser Seite)

// 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
quelle
13
Das ist großartig, aber es gibt eine wichtige Einschränkung: Wenn ein Array wie geändert wird arr[2] = "foo", ist die Änderungsbenachrichtigung asynchron . Da JS keine Möglichkeit bietet, nach solchen Änderungen zu suchen, basiert diese Bibliothek auf einem Timeout, das alle 250 ms ausgeführt wird und prüft, ob sich das Array überhaupt geändert hat. Daher erhalten Sie erst beim nächsten Mal eine Änderungsbenachrichtigung Zeit läuft das Timeout. Andere Änderungen wie werden push()jedoch sofort (synchron) benachrichtigt.
Peterflynn
6
Ich denke auch, dass 250 Intervalle die Leistung Ihrer Site beeinflussen, wenn das Array groß ist.
Tomáš Zato - Wiedereinsetzung Monica
Gerade benutzt, funktioniert wie ein Zauber. Für unsere knotenbasierten Freunde habe ich diese Beschwörung mit einem Versprechen verwendet. (Format in Kommentaren ist ein Schmerz ...) _ = require ('lodash'); erfordern ("Unterstrich beobachten") ( ); Versprechen = erfordern ("Bluebird"); neues Versprechen zurückgeben (Funktion (auflösen, ablehnen) {return _.observe (Warteschlange, 'löschen', Funktion () {if ( .isEmpty (Warteschlange)) {Auflösung auflösen (Aktion);}});});
Leif
5

Ich habe den folgenden Code verwendet, um Änderungen an einem Array abzuhören.

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

Hoffe das war nützlich :)

Nadir Laskar
quelle
5

Die am besten bewertete Override-Push-Methode von @canon hat einige Nebenwirkungen, die in meinem Fall unpraktisch waren:

  • Dadurch unterscheidet sich der Push-Eigenschaftendeskriptor ( writableund configurablesollte trueanstelle von festgelegt werden false), was zu einem späteren Zeitpunkt zu Ausnahmen führt.

  • Es löst das Ereignis mehrmals aus, wenn push()es einmal mit mehreren Argumenten (z. B. myArray.push("a", "b")) aufgerufen wird , was in meinem Fall unnötig und für die Leistung schlecht war.

Dies ist also die beste Lösung, die ich finden konnte, um die vorherigen Probleme zu beheben, und meiner Meinung nach sauberer / einfacher / leichter zu verstehen.

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

Bitte beachten Sie die Kommentare zu meinen Quellen und Hinweise zur Implementierung der anderen Mutationsfunktionen außer Push: 'Pop', 'Shift', 'Unshift', 'Splice', 'Sort', 'Reverse'.

cprcrack
quelle
@canon Ich habe Proxies zur Verfügung, aber ich kann sie nicht verwenden, da das Array extern geändert wird, und ich kann mir keine Möglichkeit vorstellen, die externen Anrufer (die sich von Zeit zu Zeit ohne meine Kontrolle ändern) zur Verwendung eines Proxys zu zwingen .
Cprcrack
@canon und übrigens, Ihr Kommentar hat mich zu einer falschen Annahme gebracht, nämlich dass ich den Spread-Operator verwende, obwohl ich es eigentlich nicht bin. Also nein, ich nutze den Spread-Operator überhaupt nicht. Was ich verwende, ist der Rest-Parameter, der eine ähnliche ...Syntax hat und der leicht durch die Verwendung des argumentsSchlüsselworts ersetzt werden kann.
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
quelle
1
Sieht aus wie Object.observe()und Array.observe()wurden aus der Spezifikation zurückgezogen. Die Unterstützung wurde bereits von Chrome übernommen. : /
Kanon
0

Ich bin mir nicht sicher, ob dies absolut alles abdeckt, aber ich verwende so etwas (insbesondere beim Debuggen), um festzustellen, wann einem Array ein Element hinzugefügt wurde:

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
quelle
-1

Eine interessante Sammlungsbibliothek ist https://github.com/mgesmundo/smart-collection . Ermöglicht das Anzeigen von Arrays und das Hinzufügen von Ansichten. Ich bin mir nicht sicher über die Leistung, da ich sie selbst teste. Wird diesen Beitrag bald aktualisieren.

Kontinuität
quelle
-1

Ich spielte herum und fand das. Die Idee ist, dass für das Objekt alle Array.prototype-Methoden definiert sind, diese jedoch auf einem separaten Array-Objekt ausgeführt werden. Dies gibt die Möglichkeit, Methoden wie shift (), pop () usw. zu beobachten. Einige Methoden wie concat () geben das OArray-Objekt jedoch nicht zurück. Durch Überladen dieser Methoden wird das Objekt nicht sichtbar, wenn Accessoren verwendet werden. Um letzteres zu erreichen, werden die Accessoren für jeden Index innerhalb einer bestimmten Kapazität definiert.

Leistungstechnisch ... OArray ist im Vergleich zum einfachen Array-Objekt etwa 10 bis 25 Mal langsamer. Für die Kapazität in einem Bereich von 1 bis 100 beträgt die Differenz 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
quelle
Während es auf vorhandenen Elementen funktioniert, funktioniert es nicht, wenn ein Element mit dem Array [new_index] = value hinzugefügt wird. Das können nur Proxys.
mpm
-5

Ich würde Ihnen nicht empfehlen, native Prototypen zu erweitern. Stattdessen können Sie eine Bibliothek wie new-list verwenden. https://github.com/azer/new-list

Es erstellt ein natives JavaScript-Array und ermöglicht es Ihnen, Änderungen zu abonnieren. Es stapelt die Aktualisierungen und gibt Ihnen den endgültigen Unterschied.

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
quelle