Comment surveiller les changements de tableau?


107

En Javascript, existe-t-il un moyen d'être averti lorsqu'un tableau est modifié à l'aide de l'affectation push, pop, shift ou indexée? Je veux quelque chose qui déclencherait un événement que je pourrais gérer.

Je connais la watch()fonctionnalité de SpiderMonkey, mais cela ne fonctionne que lorsque la variable entière est définie sur autre chose.

Réponses:


171

Il y a quelques options ...

1. Remplacer la méthode push

En empruntant la voie rapide et sale, vous pouvez remplacer la push()méthode de votre tableau 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 Sinon, si vous souhaitez cibler tous les tableaux, vous pouvez remplacer Array.prototype.push(). Soyez prudent, cependant; les autres codes de votre environnement peuvent ne pas aimer ou ne pas s'attendre à ce type de modification. Pourtant, si un fourre-tout semble attrayant, remplacez-le simplement myArraypar Array.prototype.

Maintenant, ce n'est qu'une méthode et il existe de nombreuses façons de modifier le contenu du tableau. Nous avons probablement besoin de quelque chose de plus complet ...

2. Créez un tableau observable personnalisé

Plutôt que de remplacer les méthodes, vous pouvez créer votre propre tableau observable. Ce particulier des copies de mise en œuvre d' un tableau dans un nouvel objet semblable à un tableau et fournit la coutume push(), pop(), shift(), unshift(), slice()etsplice() les méthodes ainsi que accesseurs d'index personnalisé ( à condition que la taille du tableau est modifié que par l' une des méthodes mentionnées ci - dessus ou la lengthpropriété).

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

})();

Voir pour référence.Object.defineProperty()

Cela nous rapproche mais ce n'est toujours pas à l'épreuve des balles ... ce qui nous amène à:

3. Proxies

Proxies offrent une autre solution ... vous permettant d'intercepter les appels de méthode, les accesseurs, etc. Plus important encore, vous pouvez le faire sans même fournir un nom de propriété explicite ... ce qui vous permettrait de tester un accès arbitraire basé sur un index / affectation. Vous pouvez même intercepter la suppression de propriété. Les procurations vous permettraient effectivement d'inspecter un changement avant de décider de l'autoriser ... en plus de gérer le changement après coup.

Voici un exemple simplifié:

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

})();


Merci! Cela fonctionne pour les méthodes de tableau régulières. Des idées sur la façon de créer un événement pour quelque chose comme "arr [2] =" foo "?
Sridatta Thatipamala

4
Je suppose que vous pourriez implémenter une méthode set(index)dans le prototype d'Array et faire quelque chose comme l'antisanité dit
Pablo Fernandez

8
Il serait bien préférable de sous-classer Array. Ce n'est généralement pas une bonne idée de modifier le prototype d'Array.
Wayne

1
Réponse exceptionnelle ici. La classe de ObservableArray est excellente. +1
dooburt

1
"'_array.length === 0 && delete _self [index];" - pouvez-vous expliquer cette ligne?
splintor

23

En lisant toutes les réponses ici, j'ai assemblé une solution simplifiée qui ne nécessite aucune bibliothèque externe.

Il illustre également beaucoup mieux l'idée générale de l'approche:

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

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

C'est une bonne idée, mais ne pensez-vous pas que si, par exemple, je veux implémenter cela dans les tableaux de données du graphique js, et que j'ai 50 graphiques, ce qui signifie 50 tableaux et chaque tableau sera mis à jour toutes les secondes -> imaginez la taille de le tableau 'myEventsQ' à la fin de la journée! Je pense que quand besoin de le changer de temps en temps
Yahya

2
Vous ne comprenez pas la solution. myEventsQ EST le tableau (l'un de vos 50 tableaux). Cet extrait de code ne modifie pas la taille du tableau, et n'ajoute aucun tableau supplémentaire, il ne modifie que le prototype des tableaux existants.
Sych

1
mmmm je vois, plus d'explications auraient dû être fournies cependant!
Yahya le

3
pushrenvoie le lengthdu tableau. Ainsi, vous pouvez obtenir la valeur renvoyée par Array.prototype.push.applyà une variable et la renvoyer à partir de la pushfonction personnalisée .
adiga le

12

J'ai trouvé ce qui suit qui semble accomplir cela: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays étend le trait de soulignement et peut être utilisé comme suit: (à partir de cette page)

// 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
C'est génial, mais il y a une mise en garde importante: quand un tableau est modifié comme arr[2] = "foo", la notification de changement est asynchrone . Étant donné que JS ne fournit aucun moyen de surveiller de tels changements, cette bibliothèque repose sur un délai d'expiration qui s'exécute toutes les 250 ms et vérifie si le tableau a changé du tout - vous ne recevrez donc pas de notification de modification avant le prochain heure d'expiration du délai. push()Cependant, d' autres changements comme être notifié immédiatement (de manière synchrone).
peterflynn

6
Aussi, je suppose que 250 intervalles affecteront les performances de votre site si le tableau est grand.
Tomáš Zato - Réintégrer Monica

Je viens de l'utiliser, fonctionne comme un charme. Pour nos amis basés sur les nœuds, j'ai utilisé cette incantation avec une promesse (le format dans les commentaires est une douleur ...) _ = require ('lodash'); require ("souligné-observer") ( ); Promise = require ("bluebird"); return new Promise (function (résoudre, rejeter) {return _.observe (queue, 'delete', function () {if ( .isEmpty (queue)) {return résoudre (action);}});});
Leif

7

J'ai utilisé le code suivant pour écouter les modifications apportées à un tableau.

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

J'espère que cela a été utile :)


6

La solution de méthode push Override la plus votée de @canon a des effets secondaires qui n'étaient pas pratiques dans mon cas:

  • Cela rend le descripteur de propriété push différent ( writableet configurabledoit être défini à la trueplace de false), ce qui provoque des exceptions ultérieurement.

  • Il déclenche l'événement plusieurs fois lorsqu'il push()est appelé une fois avec plusieurs arguments (tels que myArray.push("a", "b")), ce qui dans mon cas était inutile et mauvais pour les performances.

C'est donc la meilleure solution que j'ai pu trouver pour résoudre les problèmes précédents et qui est à mon avis plus propre / plus simple / plus facile à comprendre.

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

S'il vous plaît voir les commentaires pour mes sources et pour des conseils sur la façon d'implémenter les autres fonctions de mutation en dehors de push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.


@canon J'ai des proxys disponibles, mais je ne peux pas les utiliser car le tableau est modifié en externe, et je ne vois aucun moyen de forcer les appelants externes (qui en plus changent de temps en temps sans mon contrôle) à utiliser un proxy .
cprcrack

@canon et au fait, votre commentaire m'a fait faire une fausse hypothèse, à savoir que j'utilise l'opérateur de propagation, alors qu'en fait je ne le suis pas. Donc non, je ne tire pas du tout parti de l'opérateur d'épandage. Ce que j'utilise est le paramètre rest qui a une ...syntaxe similaire , et qui peut être facilement remplacé par l'utilisation du argumentsmot - clé.
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
Ressemble Object.observe()et a Array.observe()été retiré de la spécification. L'assistance a déjà été retirée de Chrome. : /
canon

0

Je ne sais pas si cela couvre absolument tout, mais j'utilise quelque chose comme ça (surtout lors du débogage) pour détecter lorsqu'un tableau a un élément ajouté:

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

J'ai bidouillé et j'ai trouvé ça. L'idée est que l'objet a toutes les méthodes Array.prototype définies, mais les exécute sur un objet tableau séparé. Cela donne la possibilité d'observer des méthodes comme shift (), pop () etc. Bien que certaines méthodes comme concat () ne retournent pas l'objet OArray. La surcharge de ces méthodes ne rendra pas l'objet observable si des accesseurs sont utilisés. Pour réaliser ce dernier, les accesseurs sont définis pour chaque index dans une capacité donnée.

En termes de performances ... OArray est environ 10 à 25 fois plus lent que l'objet Array ordinaire. Pour la capacité dans une plage de 1 à 100, la différence est de 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;
    }
}

Bien que cela fonctionne sur des éléments existants, cela ne fonctionne pas lorsqu'un élément est ajouté avec array [new_index] = value. Seuls les mandataires peuvent le faire.
mpm

-5

Je ne vous recommanderais pas d'étendre les prototypes natifs. Au lieu de cela, vous pouvez utiliser une bibliothèque comme new-list; https://github.com/azer/new-list

Il crée un tableau JavaScript natif et vous permet de vous abonner à tout changement. Il regroupe les mises à jour et vous donne la différence 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]

})
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.