Как следить за изменениями массива? - PullRequest
73 голосов
/ 24 февраля 2011

В Javascript, есть ли способ получать уведомления, когда массив модифицируется с помощью назначения push, pop, shift или index?Я хочу что-то, что вызвало бы событие, которое я мог бы обработать.

Я знаю о функциональности watch () в SpiderMonkey, но это работает, только когда вся переменная установлена ​​на что-то другое.

Ответы [ 10 ]

132 голосов
/ 24 февраля 2011

Есть несколько вариантов ...

1. Переопределить метод push

Пройдя по быстрому и грязному маршруту, вы можете переопределить метод push() для своего массива 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 В качестве альтернативы, если вы хотите настроить таргетинг на все массивы, вы можете переопределить Array.prototype.push(). Будьте осторожны, хотя; другой код в вашей среде может не понравиться или ожидать такой модификации. Тем не менее, если всеобщее звучит привлекательно, просто замените myArray на Array.prototype.

Теперь, это всего лишь один метод, и существует множество способов изменить содержимое массива. Нам, наверное, нужно что-то более всестороннее ...

2. Создать настраиваемый наблюдаемый массив

Вместо переопределения методов, вы можете создать свой собственный наблюдаемый массив. Эта конкретная реализация копирует массив в новый похожий на массив объект и предоставляет пользовательские push(), pop(), shift(), unshift(), slice() и splice() , а также пользовательские средства доступа к индексам (при условии, что размер массива изменяется только одним из вышеупомянутых методов или свойством 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());

})();

См. Object.<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" rel="noreferrer">defineProperty()</a> для справки.

Это приближает нас, но это еще не пуленепробиваемое ... что приводит нас к:

3. Доверенные

В будущем 1 , прокси могут предложить другое решение ... позволяющее перехватывать вызовы методов, методы доступа и т. Д. Самое главное, вы можете сделать это, даже не предоставив явное имя свойства ... которое позволит вам проверить произвольный доступ / назначение на основе индекса. Вы даже можете перехватить удаление свойства. Прокси фактически позволят вам проверить изменение до , решив разрешить его ... в дополнение к обработке изменения после факта.

Вот урезанный образец:

(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 Поддержка браузеров становится намного лучше, но все еще есть заметные дыры.

12 голосов
/ 11 ноября 2012

Я нашел следующее, которое, кажется, выполняет это: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays расширяет подчеркивание и может использоваться следующим образом: (с этой страницы)

// 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 голосов
/ 22 июля 2015

Прочитав все ответы здесь, я собрал упрощенное решение, которое не требует каких-либо внешних библиотек.

Это также намного лучше иллюстрирует общую идею подхода:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
3 голосов
/ 10 ноября 2018

Наиболее одобренный Метод переопределения push Решение @canon имеет некоторые побочные эффекты, которые были неудобны в моем случае:

  • Это отличает дескриптор свойства push(writable и configurable должны быть установлены true вместо false), что вызывает исключения в более поздней точке.

  • Вызывает событие несколько раз, когда push() вызывается один раз с несколькими аргументами (например, myArray.push("a", "b")), что в моем случае было излишним и отрицательно сказывалось на производительности.

Так что это лучшее решение, которое я мог найти для исправленияпредыдущие проблемы и, на мой взгляд, чище / проще / легче для понимания.

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

Пожалуйста, смотрите комментарии к моим источникам и советы о том, как реализовать другие функции мутирования, кроме push: 'pop', 'shift ',' unshift ',' splice ',' sort ',' reverse '.

3 голосов
/ 23 мая 2018

Я использовал следующий код для прослушивания изменений в массиве.

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

Надеюсь, это было полезно:)

0 голосов
/ 06 августа 2018

Я возился и придумал это. Идея состоит в том, что объект имеет все определенные методы Array.prototype, но выполняет их в отдельном объекте массива. Это дает возможность наблюдать такие методы, как shift (), pop () и т. Д. Хотя некоторые методы, такие как concat (), не возвращают объект OArray. Перегрузка этих методов не сделает объект заметным, если используются методы доступа. Для достижения последнего, средства доступа определены для каждого индекса в пределах заданной емкости.

Быстродействие ... OArray примерно в 10-25 раз медленнее, чем обычный объект Array. Для емкости в диапазоне 1 - 100 разница составляет 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;
    }
}
0 голосов
/ 19 июля 2018

Не уверен, что это охватывает абсолютно все, но я использую что-то вроде этого (особенно при отладке), чтобы определить, когда в массив добавлен элемент:

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 голосов
/ 30 января 2015

Интересная библиотека коллекций: https://github.com/mgesmundo/smart-collection. Позволяет вам просматривать массивы и добавлять представления к ним.Не уверен насчет производительности, так как сам тестирую.Скоро обновлю этот пост.

0 голосов
/ 05 декабря 2014
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 голосов
/ 18 апреля 2013

Я бы не рекомендовал вам расширять нативные прототипы. Вместо этого вы можете использовать библиотеку, такую ​​как new-list; https://github.com/azer/new-list

Создает собственный массив JavaScript и позволяет подписаться на любые изменения. Пакет обновлений и дает вам окончательный diff;

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]

})
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...