Как следить за изменениями массива?

107

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

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

Шридатта Татипамала
источник

Ответы:

171

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

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.defineProperty()

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

3. Прокси

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

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

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

})();

каноник
источник
Спасибо! Это работает для обычных методов массива. Есть идеи, как поднять событие для чего-то вроде "arr [2] =" foo "?
Шридатта Тхтипамала,
4
Я предполагаю, что вы могли бы реализовать метод set(index)в прототипе Array и сделать что-то вроде антисанитарных требований,
Пабло Фернандес
8
Было бы намного лучше создать подкласс Array. Обычно не рекомендуется изменять прототип Array.
Уэйн
1
Отличный ответ здесь. Класс ObservableArray отличный. +1
dooburt
1
"'_array.length === 0 && delete _self [index];" - Вы можете объяснить эту строчку?
splintor
23

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

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

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
Сыч
источник
Это хорошая идея, но разве вы не думаете, что если, например, я хочу реализовать это в массивах данных диаграммы js, и у меня есть 50 диаграмм, что означает 50 массивов, и каждый массив будет обновляться каждую секунду -> представьте размер массив myEventsQ в конце дня! Я думаю, когда нужно менять его время от времени
Яхья
2
Вы не понимаете решения. myEventsQ - это массив (один из ваших 50 массивов). Этот сниппет не меняет размер массива и не добавляет никаких дополнительных массивов, он только изменяет прототип существующих.
Сыч
1
мммм, я вижу, хотя следовало дать больше объяснений!
Яхья
3
pushвозвращает lengthмассив. Таким образом, вы можете получить значение, возвращаемое Array.prototype.push.applyпеременной, и вернуть его из пользовательской pushфункции.
adiga
12

Я нашел следующее, которое, кажется, выполняет это: 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');
});
user1029744
источник
13
Это здорово, но есть важное предостережение: когда массив изменяется, например arr[2] = "foo", уведомление об изменении является асинхронным . Поскольку JS не предоставляет возможности отслеживать такие изменения, эта библиотека полагается на тайм-аут, который запускается каждые 250 мс и проверяет, изменился ли вообще массив, поэтому вы не получите уведомление об изменении до следующего время истечения тайм-аута. Однако другие изменения, такие как push()получение уведомлений немедленно (синхронно).
peterflynn
6
Также я предполагаю, что интервал 250 повлияет на производительность вашего сайта, если массив большой.
Томаш Зато - Восстановите Монику
Просто использовал это, работает как шарм. Для наших друзей, основанных на узлах, я использовал это заклинание с обещанием. (Формат в комментариях - боль ...) _ = require ('lodash'); require ("подчеркивание-наблюдение") ( ); Обещание = требуется ("синяя птица"); return new Promise (функция (разрешить, отклонить) {return _.observe (очередь, 'удалить', функция () {if ( .isEmpty (queue)) {return resolve (action);}});});
Лейф
7

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

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

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

Надир Ласкар
источник
6

Наиболее upvoted Override толчок метод решения по @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'.

cprcrack
источник
@canon У меня есть прокси, но я не могу их использовать, потому что массив изменен извне, и я не могу придумать никакого способа заставить внешних вызывающих (которые, помимо того, что время от времени меняются без моего контроля), использовать прокси .
cprcrack
@canon и, кстати, ваш комментарий заставил меня сделать неправильное предположение, а именно, что я использую оператор распространения, хотя на самом деле это не так. Так что нет, я вообще не использую оператор спреда. Я использую параметр rest с аналогичным ...синтаксисом, который можно легко заменить с помощью argumentsключевого слова.
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
Похоже Object.observe()и Array.observe()были сняты со спец. Поддержка уже отключена от Chrome. : /
canon
0

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

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
источник
-1

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

непрерывность
источник
-1

Я повозился и придумал это. Идея состоит в том, что для объекта определены все методы 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;
    }
}
сисаксис
источник
Хотя он работает с существующими элементами, он не работает, когда элемент добавляется с массивом [new_index] = value. Это могут делать только прокси.
mpm
-5

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

Он создает собственный массив JavaScript и позволяет подписаться на любые изменения. Он пакетирует обновления и дает вам окончательную разницу;

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]

})
Азер
источник