Як слідкувати за змінами масиву?


106

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

Я знаю про watch()функціональність в SpiderMonkey, але це працює лише тоді, коли вся змінна встановлена ​​на щось інше.

Відповіді:


169

Є кілька варіантів ...

1. Перевизначення методу натискання

Йдучи швидким і брудним маршрутом, ви можете замінити 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 [індекс];" - Ви можете пояснити цей рядок?
спринтер

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' наприкінці дня! Я думаю, що коли-небудь потрібно це змінити
Yahya

2
Ви не розумієте рішення. myEventsQ IS масив (один з ваших 50 масивів). Цей фрагмент не змінює розмір масиву і не додає додаткових масивів, він лише змінює прототип існуючих.
Січ

1
мммм, бачу, хоч би було надано більше пояснень!
Ях’я

3
pushповертає lengthмасив. Таким чином, ви можете отримати значення, повернене Array.prototype.push.applyзмінною, і повернути її з користувацької pushфункції.
адига

12

Я знайшов таке, що, здається, це досягає: https://github.com/mennovanslooten/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');
});

13
Це чудово, але є важливий застереження: коли масив змінюється на зразок arr[2] = "foo", сповіщення про зміну є асинхронним . Оскільки JS не забезпечує жодного способу спостереження за такими змінами, ця бібліотека покладається на тайм-аут, який працює кожні 250 мс і перевіряє, чи змінився масив взагалі - тому ви не отримаєте сповіщення про зміну до наступного час, що проходить тайм-аут. push()Однак про інші зміни, наприклад, отримують сповіщення негайно (синхронно).
peterflynn

6
Також я думаю, що інтервал 250 вплине на продуктивність вашого сайту, якщо масив великий.
Томаш Зато - Відновіть Моніку

Щойно використане це, працює як шарм. Для наших друзів, що базуються на вузлах, я використав цю заклик із обіцянкою. вимагати ("підкреслити-спостерігати") ( ); Обіцяти = вимагати ("синій птах"); повернути нове Promise (функція (вирішити, відхилити) {return _.observe (черга, 'видалити', функція () {if ( .isEmpty (черга)) {return resolution (action);}});});
Лейф

5

Я використовував наступний код для прослуховування змін у масиві.

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

Сподіваюся, це було корисно :)


5

Найбільш схвалене рішення методу 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', 'unhift', 'сплайс', 'сортування', 'reverse'.


@canon У мене є доступні проксі, але я не можу їх використовувати, оскільки масив змінюється зовні, і я не можу придумати жодного способу змусити зовнішніх абонентів (які окрім змін час від часу без мого контролю) використовувати проксі .
cprcrack

@canon, і до речі, ваш коментар змусив мене зробити неправильне припущення, а саме те, що я використовую оператор розповсюдження, коли насправді я це не так. Так що ні, я взагалі не використовую оператор розповсюдження. Я використовую параметр "решта", який має подібний ...синтаксис, і який можна легко замінити використанням 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;
    }
});


-1

Я спіткнувся і придумав це. Ідея полягає в тому, що в об’єкті визначені всі методи Array.prototype, але він виконує їх на окремому об'єкті масиву. Це дає можливість спостерігати за такими методами, як shift (), pop () тощо. Хоча деякі методи, такі як concat (), не повертають об'єкт OArray. Перевантаження цих методів не зробить об'єкт помітним, якщо використовуються аксесуари. Для досягнення останнього визначаються аксесуари для кожного індексу в межах заданої потужності.

Ефективність ... OArray приблизно в 10-25 разів повільніше порівняно з простим об'єктом Array. Для копальності в діапазоні 1 - 100 різниця становить 1х-3х.

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] = значення. Це можуть зробити лише проксі.
mpm

-5

Я б не рекомендував вам поширювати нативний прототип. Натомість ви можете використовувати бібліотеку, як новий список; 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]

})
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.