web-dev-qa-db-fra.com

Comment surveiller les changements de tableau?

En Javascript, existe-t-il un moyen d’être averti lorsqu’un tableau est modifié à l’aide des assignations Push, Pop, Shift ou Index? Je veux quelque chose qui déclencherait un événement que je pourrais gérer.

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

68
Sridatta Thatipamala

Il y a quelques options ...

1. Ignorer la méthode Push

En passant par la route rapide et sale, vous pouvez remplacer la méthode Push() pour 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 Si vous souhaitez cibler les tableaux tous, vous pouvez également remplacer Array.prototype.Push(). Soyez prudent, cependant; d'autres codes de votre environnement peuvent ne pas aimer ou s'attendre à ce type de modification. Néanmoins, si un attrait semble attrayant, remplacez simplement myArray par Array.prototype.

Maintenant, ce n'est qu'une méthode et il y a beaucoup de façons de changer le contenu d'un tableau. Nous avons probablement besoin de quelque chose de plus complet ...

2. Créer un tableau observable personnalisé

Plutôt que de surcharger des méthodes, vous pouvez créer votre propre tableau observable. Cette implémentation particulière copie un tableau dans un nouvel objet ressemblant à un tableau et fournit les méthodes Push(), pop(), shift(), unshift(), slice() et splice() personnalisées ainsi que des accesseurs d’index personnalisés (à condition que la taille du tableau ne soit modifiée des méthodes susmentionnées ou de la propriété length).

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 Object.defineProperty() pour référence.

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

3. Les procurations

A l'avenir1, les mandataires peuvent offrir une autre solution ... vous permettant d'intercepter les appels de méthodes, les accesseurs, etc. Plus important encore, vous pouvez le faire sans même fournir un nom de propriété explicite ... qui vous permettrait de tester une valeur arbitraire. , accès/assignation basé sur un index. Vous pouvez même intercepter la suppression de propriété. Les mandataires vous permettraient effectivement d'inspecter une modification avant décidant de l'autoriser ... en plus de la gestion de la modification après coup.

Voici un échantillon dépouillé:

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

})();

1 Prise en charge du navigateur va beaucoup mieux, mais il reste encore quelques lacunes.

122
canon

J'ai trouvé ce qui suit qui semble accomplir ceci: 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');
});
11
user1029744

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

Cela illustre aussi 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();};
10
Sych

La méthode la plus votée La méthode Push prioritaire de @Canon a des effets secondaires gênants dans mon cas:

  • Cela rend le descripteur de propriété Push différent (writable et configurable doivent être définis par true au lieu de false), ce qui provoque des exceptions à un point ultérieur.

  • Il déclenche l'événement plusieurs fois lorsque Push() est appelé une fois avec plusieurs arguments (tels que myArray.Push("a", "b")), ce qui dans mon cas était inutile et nuisait à la performance.

Donc, c’est la meilleure solution que j’ai pu trouver qui résout les problèmes précédents et qui, à mon avis, est 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
    }
});

Veuillez consulter les commentaires relatifs à mes sources et aux astuces sur la manière d'implémenter les autres fonctions de mutation, à l'exception de Push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.

3
cprcrack

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 c'était utile :)

2
Nadir Laskar

Je ne sais pas si cela couvre absolument tout, mais j'utilise quelque chose comme ceci (surtout lors du débogage) pour détecter quand 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;
    }
});
0
user3337629
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);
0
Martin Wantke

Une bibliothèque de collection intéressante est https://github.com/mgesmundo/smart-collection . Vous permet de regarder des tableaux et d’ajouter des vues à ceux-ci. Pas sûr de la performance, je la teste moi-même. Mettra à jour ce post bientôt.

0
kontinuity