Dizi değişiklikleri nasıl izlenir?


111

Javascript'te, bir dizi push, pop, shift veya indeks tabanlı atama kullanılarak değiştirildiğinde bildirim almanın bir yolu var mı? Başa çıkabileceğim bir olayı tetikleyecek bir şey istiyorum.

watch()SpiderMonkey'deki işlevselliği biliyorum , ancak bu yalnızca tüm değişken başka bir şeye ayarlandığında çalışır.

Yanıtlar:


174

Birkaç seçenek var ...

1. İtme yöntemini geçersiz kılın

Hızlı ve kirli rota giderek, size dizisi için `) (yöntemini` itmek geçersiz olabilir 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 Alternatif olarak, tüm dizileri hedeflemek isterseniz, geçersiz kılabilirsiniz Array.prototype.push(). Yine de dikkatli olun; çevrenizdeki diğer kodlar bu tür değişiklikleri beğenmeyebilir veya beklemeyebilir. Tümünü yakalama sesler çekici, hala, sadece değiştirmek myArrayile Array.prototype.

Şimdi, bu sadece bir yöntem ve dizi içeriğini değiştirmenin birçok yolu var. Muhtemelen daha kapsamlı bir şeye ihtiyacımız var ...

2. Özel bir gözlemlenebilir dizi oluşturun

Yöntemleri geçersiz kılmak yerine, kendi gözlemlenebilir dizinizi yaratabilirsiniz. Bu özel uygulama, bir diziyi yeni bir dizi benzeri nesneye kopyalar ve özel 'push ()', `pop () ',` shift ()', ʻunshift () ', `slice ()' ve` splice ( ) `yöntemler ** ve ** özel dizin erişimcileri (dizi boyutunun yalnızca yukarıda belirtilen yöntemlerden biri veya" uzunluk "özelliği aracılığıyla değiştirilmesi koşuluyla).

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

})();

Referans için bakın .Object.defineProperty()

Bu bizi yaklaştırıyor ama yine de kurşun geçirmez değil ... bu da bizi şu noktalara getiriyor:

3. Vekiller

Bir Proxy nesnesi , modern tarayıcıya başka bir çözüm sunar . Yöntem çağrılarını, erişimcileri vb. Engellemenize olanak tanır. En önemlisi, bunu açık bir özellik adı bile sağlamadan yapabilirsiniz ... bu da keyfi, indeks tabanlı bir erişim / atamayı test etmenize olanak sağlar. Hatta mülkün silinmesine müdahale edebilirsiniz. Proxy'ler, değişikliğe izin vermeye karar vermeden önce bir değişikliği incelemenizi etkili bir şekilde sağlar ...

İşte soyulmuş bir örnek:

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

})();


Teşekkürler! Bu, normal dizi yöntemleri için işe yarar. Böyle bir şey için bir etkinlik yükseltmek konusunda herhangi bir fikir "arr [2] = 'foo'?
Sridatta Thatipamala

4
Sanırım set(index)Array'in prototipinde bir yöntem uygulayabilir ve antisanity'nin söylediği gibi bir şey yapabilirsiniz
Pablo Fernandez

8
Array'i alt sınıflara ayırmak çok daha iyi olurdu. Array'in prototipini değiştirmek genellikle iyi bir fikir değildir.
Wayne

1
Burada olağanüstü cevap. ObservableArray'ın sınıfı mükemmel. +1
dooburt

1
"'_array.length === 0 && _self'i sil [dizin];" - bu satırı açıklayabilir misin?
splintor

23

Buradaki tüm cevapları okuduktan sonra, herhangi bir harici kitaplık gerektirmeyen basitleştirilmiş bir çözüm oluşturdum.

Ayrıca, yaklaşımın genel fikrini çok daha iyi göstermektedir:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};

Bu iyi bir fikir, ancak örneğin bunu grafik js veri dizilerine uygulamak istersem ve 50 grafiğim varsa, bu da 50 dizi anlamına gelir ve her dizinin her saniye güncelleneceğini düşünmeyin -> Günün sonunda 'myEventsQ' dizisi! Arada sırada onu değiştirmem gerektiğini düşünüyorum
Yahya

2
Çözümü anlamıyorsun. myEventsQ dizidir (50 dizinizden biri). Bu kod parçası, dizinin boyutunu değiştirmez ve herhangi bir ek dizi eklemez, yalnızca mevcut olanların prototipini değiştirir.
Sych

1
mmmm Anlıyorum, yine de daha fazla açıklama sağlanmalıydı!
Yahya

3
pushlengthdizinin değerini döndürür . Böylece, Array.prototype.push.applybir değişkene döndürülen değeri alabilir ve özel pushişlevden döndürebilirsiniz .
adiga

12

Bunu başarıyor gibi görünen aşağıdakileri buldum: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arrays, alt çizgiyi genişletir ve şu şekilde kullanılabilir: (bu sayfadan)

// 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
Bu harika, ancak önemli bir uyarı var: bir dizi gibi arr[2] = "foo"değiştirildiğinde, değişiklik bildirimi eşzamansızdır . JS bu tür değişiklikleri izlemenin herhangi bir yolunu sağlamadığından, bu kitaplık her 250 ms'de bir çalışan bir zaman aşımına dayanır ve dizinin hiç değişip değişmediğini kontrol eder - böylece bir sonrakine kadar bir değişiklik bildirimi almazsınız zaman aşımı süresi. push()Bununla birlikte, diğer değişiklikler anında (eşzamanlı olarak) bildirilir.
peterflynn

6
Ayrıca dizi büyükse 250 aralığının site performansınızı etkileyeceğini tahmin ediyorum.
Tomáš Zato -

Sadece bunu kullandım, cazibe gibi çalışıyor. Düğüm tabanlı arkadaşlarımız için bu büyülü sözü bir sözle kullandım. (Yorumlarda biçim bir acıdır ...) _ = required ('lodash'); need ("alt çizgi-gözlemle") ( ); Söz = gerekli ("mavi kuş"); yeni Promise (işlev (çözümleme, reddetme) döndürme _.observe (sıra, 'sil', işlev () {if ( .isEmpty (sıra)) {dönüş çözme (eylem);}});});
Leif

7

Bir dizideki değişiklikleri dinlemek için aşağıdaki kodu kullandım.

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

Umarım bu yararlı olmuştur :)


6

@Canon tarafından en çok oylanan Geçersiz Kılma itme yöntemi çözümü benim durumumda rahatsız edici olan bazı yan etkilere sahiptir:

  • Push özelliği tanımlayıcısını farklı kılar ( writableve bunun yerine configurableayarlanmalıdır ), bu da daha sonraki bir noktada istisnalara neden olur.truefalse

  • Bu, benim durumumda gereksiz ve performans açısından kötü push()olan birden çok bağımsız değişkenle (örneğin myArray.push("a", "b")) bir kez çağrıldığında olayı birden çok kez gündeme getiriyor .

Bu, önceki sorunları çözen bulabildiğim en iyi çözüm ve bence daha temiz / daha basit / anlaşılması daha kolay.

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

Lütfen kaynaklarımın yorumlarına ve push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' dışında diğer mutasyon işlevlerinin nasıl uygulanacağına dair ipuçlarına bakın.


@canon Kullanılabilir Proxy'lerim var, ancak bunları kullanamıyorum çünkü dizi harici olarak değiştirildi ve harici arayanları (bu zaman zaman kontrolüm olmadan değişmenin yanı sıra) bir Proxy kullanmaya zorlamanın herhangi bir yolunu düşünemiyorum .
cprcrack

@canon ve bu arada, yorumunuz bana yanlış bir varsayımda bulundu, ki aslında ben olmadığım halde yayılma operatörünü kullanıyorum. Yani hayır, yayılma operatöründen hiç yararlanmıyorum. Kullandığım şey, benzer bir ...sözdizimine sahip olan ve argumentsanahtar kelimenin kullanımıyla kolayca değiştirilebilen rest parametresidir .
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
Dış görünüş gibi Object.observe()ve Array.observe()spec çekildi. Destek, Chrome'dan zaten alındı. : /
canon

0

Bunun kesinlikle her şeyi kapsayıp kapsamadığından emin değilim, ancak bir dizide bir eleman eklendiğinde bunu tespit etmek için (özellikle hata ayıklarken) böyle bir şey kullanıyorum:

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

Etrafta oynadım ve bunu buldum. Buradaki fikir, nesnenin tanımlanmış tüm Array.prototype yöntemlerine sahip olması, ancak bunları ayrı bir dizi nesnesinde çalıştırmasıdır. Bu, shift (), pop () vb. Gibi yöntemleri gözlemleme yeteneği sağlar. Ancak concat () gibi bazı yöntemler OArray nesnesini döndürmez. Bu yöntemlerin aşırı yüklenmesi, erişimciler kullanılıyorsa nesneyi gözlemlenebilir yapmaz. İkincisini başarmak için, erişimciler belirli kapasite dahilindeki her dizin için tanımlanır.

Performans açısından ... OArray, düz Array nesnesine kıyasla yaklaşık 10-25 kat daha yavaştır. 1-100 aralığındaki kapasite için fark 1x-3x'tir.

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

Mevcut elemanlar üzerinde çalışırken, [new_index] = değer dizisi ile bir eleman eklendiğinde çalışmaz. Bunu yalnızca vekiller yapabilir.
mpm

-5

Yerel prototipleri genişletmenizi tavsiye etmem. Bunun yerine, yeni liste gibi bir kitaplık kullanabilirsiniz; https://github.com/azer/new-list

Yerel bir JavaScript dizisi oluşturur ve herhangi bir değişikliğe abone olmanızı sağlar. Güncellemeleri toplu hale getirir ve size son farkı verir;

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]

})
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.