Jak obserwować zmiany w tablicy?

106

Czy w Javascript istnieje sposób powiadamiania o modyfikacji tablicy za pomocą przypisania push, pop, shift lub indeksowego? Chcę czegoś, co uruchomiłoby wydarzenie, z którym mógłbym sobie poradzić.

Wiem o watch()funkcjonalności w SpiderMonkey, ale działa to tylko wtedy, gdy cała zmienna jest ustawiona na coś innego.

Sridatta Thatipamala
źródło

Odpowiedzi:

169

Jest kilka opcji ...

1. Zastąp metodę wypychania

Wybierając szybką i brudną trasę, możesz zastąpić push()metodę dla swojej tablicy 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 Alternatywnie, jeśli chcesz kierować reklamy na wszystkie tablice, możesz przesłonić Array.prototype.push(). Należy jednak zachować ostrożność; inny kod w twoim środowisku może nie lubić lub oczekiwać tego rodzaju modyfikacji. Jeśli jednak hasło typu catch-all brzmi zachęcająco, po prostu zastąp myArraygo Array.prototype.

To tylko jedna metoda i istnieje wiele sposobów zmiany zawartości tablicy. Prawdopodobnie potrzebujemy czegoś bardziej wszechstronnego ...

2. Utwórz niestandardową obserwowalną tablicę

Zamiast przesłonić metody, możesz utworzyć własną obserwowalną tablicę. W tej konkretnej realizacji kopii tablicy w nową tablicę jak przedmiot i dostarcza na zamówienie push(), pop(), shift(), unshift(), slice(), i splice()sposoby , jak również dostępowych Indeksu (pod warunkiem, że rozmiar tablicy zmienia się tylko za pomocą jednego z wyżej wymienionych metod lub lengthwłasności).

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

})();

Zobacz w celach informacyjnych.Object.defineProperty()

To przybliża nas, ale wciąż nie jest kuloodporne ... co prowadzi nas do:

3. Proxy

Serwery proxy oferują inne rozwiązanie ... pozwalające na przechwytywanie wywołań metod, akcesorów itp. Co najważniejsze, można to zrobić bez podawania nawet jawnej nazwy właściwości ... która pozwoliłaby na przetestowanie dowolnego dostępu opartego na indeksach / zadanie. Możesz nawet przechwycić usunięcie właściwości. Serwery proxy skutecznie umożliwiłyby sprawdzenie zmiany przed podjęciem decyzji o zezwoleniu ... oprócz obsługi zmiany po fakcie.

Oto okrojona próbka:

(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
źródło
Dzięki! Działa to w przypadku zwykłych metod tablicowych. Jakieś pomysły, jak zgłosić wydarzenie na przykład „arr [2] =„ foo ”?
Sridatta Thatipamala,
4
Myślę, że mógłbyś zaimplementować metodę set(index)w prototypie Arraya i zrobić coś takiego, jak mówi antyspołeczność
Pablo Fernandez
8
Byłoby znacznie lepiej, gdybyśmy utworzyli podklasę Array. Zwykle modyfikowanie prototypu Arraya nie jest dobrym pomysłem.
Wayne
1
Znakomita odpowiedź tutaj. Klasa ObservableArray jest doskonała. +1
dooburt
1
"'_array.length === 0 && delete _self [index];" - czy możesz wyjaśnić tę kwestię?
splintor
23

Czytając wszystkie odpowiedzi tutaj, zebrałem uproszczone rozwiązanie, które nie wymaga żadnych zewnętrznych bibliotek.

To również znacznie lepiej ilustruje ogólną ideę podejścia:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
Sych
źródło
To dobry pomysł, ale nie sądzisz, że jeśli na przykład chcę to zaimplementować w tablicach danych chart js, a mam 50 wykresów, co oznacza 50 tablic i każda tablica będzie aktualizowana co sekundę -> wyobraź sobie rozmiar tablica „myEventsQ” na koniec dnia! Myślę, kiedy trzeba to zmienić od czasu do czasu
Yahya,
2
Nie rozumiesz rozwiązania. myEventsQ JEST tablicą (jedną z 50 tablic). Ten fragment kodu nie zmienia rozmiaru tablicy i nie dodaje żadnych dodatkowych tablic, zmienia tylko prototyp istniejących.
Sych
1
mmmm Widzę, należało jednak podać więcej wyjaśnień!
Yahya
3
pushzwraca lengthtablicę. Możesz więc pobrać wartość zwracaną przez Array.prototype.push.applyzmienną i zwrócić ją z pushfunkcji niestandardowej .
adiga
12

Znalazłem następujące rzeczy, które wydają się to osiągnąć: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays rozszerza podkreślenie i może być użyte w następujący sposób: (z tej strony)

// 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
źródło
13
To jest świetne, ale istnieje ważne zastrzeżenie: gdy tablica jest modyfikowana arr[2] = "foo", powiadomienie o zmianie jest asynchroniczne . Ponieważ JS nie zapewnia żadnego sposobu na obserwowanie takich zmian, ta biblioteka opiera się na limicie czasu, który jest uruchamiany co 250 ms i sprawdza, czy tablica w ogóle się zmieniła - więc nie otrzymasz powiadomienia o zmianie do następnego czas upływa limitu czasu. push()Jednak inne zmiany, takie jak natychmiastowe otrzymywanie powiadomień (synchronicznie).
peterflynn
6
Wydaje mi się, że interwał 250 wpłynie na wydajność witryny, jeśli tablica jest duża.
Tomáš Zato - Przywróć Monikę
Właśnie to użyłem, działa jak urok. Dla naszych przyjaciół z węzłów użyłem tego zaklęcia z obietnicą. (Format w komentarzach jest uciążliwy ...) _ = require ('lodash'); require ("podkreślenie-obserwuj") ( ); Promise = require ("bluebird"); return new Promise (funkcja (rozwiąż, odrzuć) {return _.observe (kolejka, 'usuń', funkcja () {if ( .isEmpty (kolejka)) {return rozwiąż (akcja);}});});
Leif
5

Użyłem następującego kodu, aby nasłuchiwać zmian w tablicy.

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

Mam nadzieję, że to było przydatne :)

Nadir Laskar
źródło
5

Najpopularniejsze rozwiązanie metody Override push autorstwa @canon ma pewne skutki uboczne, które były niewygodne w moim przypadku:

  • To sprawia, że ​​deskryptor właściwości push jest inny ( writablei configurablepowinien być ustawiony truezamiast false), co powoduje wyjątki w późniejszym punkcie.

  • Wywołuje zdarzenie wiele razy, gdy push()jest wywoływane raz z wieloma argumentami (takimi jak myArray.push("a", "b")), co w moim przypadku było niepotrzebne i niekorzystne dla wydajności.

Jest to więc najlepsze rozwiązanie, jakie znalazłem, które rozwiązuje poprzednie problemy i jest moim zdaniem czystsze / prostsze / łatwiejsze do zrozumienia.

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

Proszę zapoznać się z komentarzami do moich źródeł i wskazówkami, jak zaimplementować inne funkcje mutujące oprócz push: „pop”, „shift”, „unshift”, „splice”, „sort”, „reverse”.

cprcrack
źródło
@canon Mam dostępne proxy, ale nie mogę ich używać, ponieważ tablica jest modyfikowana zewnętrznie i nie mogę wymyślić żadnego sposobu, aby zmusić zewnętrznych rozmówców (które oprócz tego zmieniają się od czasu do czasu bez mojej kontroli) do korzystania z proxy .
cprcrack
@canon i przy okazji, Twój komentarz sprawił, że przyjąłem błędne założenie, że używam operatora spreadu, podczas gdy tak naprawdę nie jest. Więc nie, w ogóle nie wykorzystuję operatora spreadu. To, czego używam, to parametr rest, który ma podobną ...składnię i można go łatwo zastąpić za pomocąarguments słowa kluczowego.
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
źródło
1
Wygląda na to Object.observe()i Array.observe()zostały wycofane ze specyfikacji. Wsparcie zostało już ściągnięte z Chrome. : /
canon
0

Nie jestem pewien, czy obejmuje to absolutnie wszystko, ale używam czegoś takiego (szczególnie podczas debugowania), aby wykryć, kiedy tablica ma dodany element:

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
źródło
-1

Ciekawą biblioteką kolekcji jest https://github.com/mgesmundo/smart-collection . Umożliwia oglądanie tablic i dodawanie do nich widoków. Nie jestem pewien wydajności, ponieważ sam ją testuję. Niedługo zaktualizuję ten post.

ciągłość
źródło
-1

Bawiłem się i wymyśliłem to. Chodzi o to, że obiekt ma zdefiniowane wszystkie metody Array.prototype, ale wykonuje je na oddzielnym obiekcie tablicy. Daje to możliwość obserwowania metod takich jak shift (), pop () itp. Chociaż niektóre metody, takie jak concat (), nie zwracają obiektu OArray. Przeciążenie tych metod nie spowoduje, że obiekt będzie obserwowalny, jeśli używane są metody dostępu. Aby to osiągnąć, metody dostępu są definiowane dla każdego indeksu w ramach określonej pojemności.

Mądra wydajność ... OArray jest około 10-25 razy wolniejszy niż zwykły obiekt Array. Dla pojemności w zakresie 1 - 100 różnica wynosi 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
źródło
Chociaż działa na istniejących elementach, nie działa, gdy element jest dodawany z tablicą [nowy_index] = wartość. Mogą to zrobić tylko proxy.
mpm
-5

Nie polecałbym ci rozszerzania natywnych prototypów. Zamiast tego możesz użyć biblioteki takiej jak nowa-lista; https://github.com/azer/new-list

Tworzy natywną tablicę JavaScript i umożliwia subskrybowanie dowolnej zmiany. Grupuje aktualizacje i podaje ostateczną różnicę;

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
źródło