¿Cómo estar atento a los cambios de matriz?

106

En Javascript, ¿hay alguna forma de recibir una notificación cuando se modifica una matriz mediante push, pop, shift o asignación basada en índices? Quiero algo que desencadene un evento que pueda manejar.

Conozco la watch()funcionalidad en SpiderMonkey, pero eso solo funciona cuando toda la variable está configurada en otra cosa.

Sridatta Thatipamala
fuente

Respuestas:

169

Hay algunas opciones ...

1. Anular el método de inserción

Siguiendo la ruta rápida y sucia, puede anular el push()método para su matriz 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 Alternativamente, si desea apuntar a todas las matrices, puede anular Array.prototype.push(). Sin embargo, tenga cuidado; Es posible que a otros códigos de su entorno no les guste o no esperen ese tipo de modificación. Aún así, si un catch-all suena atractivo, simplemente reemplácelo myArraycon Array.prototype.

Ahora, ese es solo un método y hay muchas formas de cambiar el contenido de la matriz. Probablemente necesitemos algo más completo ...

2. Cree una matriz observable personalizada

En lugar de anular métodos, puede crear su propia matriz observable. Este particular copias de aplicación una matriz en una nueva matriz-como objeto y proporciona encargo push(), pop(), shift(), unshift(), slice(), y splice()métodos , así como de acceso de índice personalizado (siempre que el tamaño de la matriz solamente se modifica a través de uno de los métodos antes mencionados o la lengthpropiedad).

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

})();

Consulte como referencia.Object.defineProperty()

Eso nos acerca, pero aún no es a prueba de balas ... lo que nos lleva a:

3. Proxies

Los proxies ofrecen otra solución ... lo que le permite interceptar llamadas a métodos, accesores, etc. Lo más importante es que puede hacer esto sin siquiera proporcionar un nombre de propiedad explícito ... lo que le permitiría probar un acceso arbitrario basado en índices / asignación. Incluso puede interceptar la eliminación de propiedades. Los proxy le permitirían inspeccionar un cambio antes de decidir permitirlo ... además de manejar el cambio después del hecho.

Aquí hay una muestra simplificada:

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

})();

canon
fuente
¡Gracias! Eso funciona para los métodos de matriz regulares. ¿Alguna idea sobre cómo crear un evento para algo como "arr [2] =" foo "?
Sridatta Thatipamala
4
Supongo que podría implementar un método set(index)en el prototipo de Array y hacer algo como dice la antisanidad
Pablo Fernandez
8
Sería mucho mejor crear una subclase de Array. Por lo general, no es una buena idea modificar el prototipo de Array.
Wayne
1
Excelente respuesta aquí. La clase de ObservableArray es excelente. +1
dooburt
1
"'_array.length === 0 && eliminar _self [índice];" - ¿Puedes explicar esta línea?
splintor
23

Después de leer todas las respuestas aquí, he reunido una solución simplificada que no requiere bibliotecas externas.

También ilustra mucho mejor la idea general del enfoque:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
Sych
fuente
Esta es una buena idea, pero ¿no cree que si, por ejemplo, quiero implementar esto en las matrices de datos de chart js y tengo 50 tablas, lo que significa 50 matrices y cada matriz se actualizará cada segundo -> imagine el tamaño de la matriz 'myEventsQ' al final del día. Creo que cuando es necesario cambiarlo de vez en cuando
Yahya
2
No entiendes la solución. myEventsQ ES la matriz (una de sus 50 matrices). Este fragmento no cambia el tamaño de la matriz y no agrega matrices adicionales, solo cambia el prototipo de las existentes.
Sych
1
mmmm ya veo, ¡debería haber dado más explicación!
Yahya
3
pushdevuelve el lengthde la matriz. Por lo tanto, puede obtener el valor devuelto por Array.prototype.push.applya una variable y devolverlo desde la pushfunción personalizada .
adiga
12

Encontré lo siguiente que parece lograr esto: https://github.com/mennovanslooten/Observable-Arrays

Las matrices observables extienden el subrayado y se pueden usar de la siguiente manera: (de esa página)

// 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');
});
usuario1029744
fuente
13
Esto es genial, pero hay una advertencia importante: cuando se modifica una matriz arr[2] = "foo", la notificación de cambio es asincrónica . Dado que JS no proporciona ninguna forma de observar dichos cambios, esta biblioteca se basa en un tiempo de espera que se ejecuta cada 250 ms y verifica si la matriz ha cambiado en absoluto, por lo que no recibirá una notificación de cambio hasta el próximo tiempo se agota el tiempo de espera. push()Sin embargo, otros cambios como recibir notificaciones de inmediato (sincrónicamente).
peterflynn
6
Además, supongo que el intervalo 250 afectará el rendimiento de su sitio si la matriz es grande.
Tomáš Zato - Reincorporación a Monica
Solo usé esto, funciona como un encanto. Para nuestros amigos basados ​​en nodos, utilicé este encantamiento con una promesa. (El formato en los comentarios es una molestia ...) _ = require ('lodash'); require ("subrayado-observar") ( ); Promise = require ("pájaro azul"); return new Promise (función (resolver, rechazar) {regresar _.observe (cola, 'eliminar', función () {if ( .isEmpty (cola)) {regresar resolver (acción);}});});
Leif
5

Usé el siguiente código para escuchar los cambios en una matriz.

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

Espero que esto haya sido útil :)

Nadir Laskar
fuente
5

La solución de método push Override más votada por @canon tiene algunos efectos secundarios que fueron inconvenientes en mi caso:

  • Hace que el descriptor de la propiedad push sea diferente ( writabley configurabledebe establecerse en truelugar de false), lo que provoca excepciones en un punto posterior.

  • Genera el evento varias veces cuando push()se llama una vez con varios argumentos (como myArray.push("a", "b")), lo que en mi caso era innecesario y malo para el rendimiento.

Así que esta es la mejor solución que pude encontrar que soluciona los problemas anteriores y, en mi opinión, es más limpia / simple / más fácil de entender.

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

Consulte los comentarios de mis fuentes y sugerencias sobre cómo implementar las otras funciones mutantes además de push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.

cprcrack
fuente
@canon Tengo Proxies disponibles, pero no puedo usarlos porque la matriz se modifica externamente, y no puedo pensar en ninguna forma de forzar a las personas que llaman externas (que además cambian de vez en cuando sin mi control) a usar un Proxy .
cprcrack
@canon y, por cierto, tu comentario me hizo suponer mal, que es que estoy usando el operador de propagación, cuando en realidad no lo estoy. Entonces no, no estoy aprovechando el operador de spread en absoluto. Lo que estoy usando es el parámetro rest que tiene una ...sintaxis similar y que se puede reemplazar fácilmente con el uso de la argumentspalabra clave.
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
fuente
1
Parece Object.observe()y Array.observe()se retiraron de la especificación. Ya se ha retirado el soporte de Chrome. : /
canon
0

No estoy seguro de si esto cubre absolutamente todo, pero uso algo como esto (especialmente al depurar) para detectar cuando una matriz tiene un elemento agregado:

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;
    }
});
usuario3337629
fuente
-1

Una biblioteca de colecciones interesante es https://github.com/mgesmundo/smart-collection . Le permite ver matrices y agregarles vistas también. No estoy seguro del rendimiento, ya que lo estoy probando yo mismo. Actualizará esta publicación pronto.

kontinuity
fuente
-1

Jugueteé y se me ocurrió esto. La idea es que el objeto tenga todos los métodos Array.prototype definidos, pero los ejecute en un objeto de matriz separado. Esto da la capacidad de observar métodos como shift (), pop (), etc. Aunque algunos métodos como concat () no devolverán el objeto OArray. La sobrecarga de esos métodos no hará que el objeto sea observable si se utilizan descriptores de acceso. Para lograr esto último, los accesos se definen para cada índice dentro de la capacidad dada.

En cuanto al rendimiento ... OArray es entre 10 y 25 veces más lento en comparación con el objeto Array simple. Para la capaidad en un rango de 1 a 100, la diferencia es 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
fuente
Si bien funciona en elementos existentes, no funciona cuando se agrega un elemento con array [new_index] = value. Solo los proxies pueden hacer eso.
mpm
-5

No le recomendaría ampliar los prototipos nativos. En su lugar, puede utilizar una biblioteca como new-list;https://github.com/azer/new-list

Crea una matriz de JavaScript nativa y le permite suscribirse a cualquier cambio. Recopila las actualizaciones y le da la diferencia final;

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
fuente