Bagaimana cara memperhatikan perubahan array?

106

Di Javascript, apakah ada cara untuk diberi tahu saat larik dimodifikasi menggunakan tugas berbasis push, pop, shift, atau indeks? Saya menginginkan sesuatu yang dapat memicu acara yang dapat saya tangani.

Saya tahu tentang watch()fungsionalitas di SpiderMonkey, tetapi itu hanya berfungsi ketika seluruh variabel disetel ke sesuatu yang lain.

Sridatta Thatipamala
sumber

Jawaban:

169

Ada beberapa pilihan ...

1. Mengesampingkan metode dorong

Menggunakan rute cepat dan kotor, Anda dapat mengganti push()metode untuk larik 1 Anda :

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 Alternatifnya, jika Anda ingin menargetkan semua larik, Anda dapat menimpa Array.prototype.push(). Berhati-hatilah; kode lain di lingkungan Anda mungkin tidak menyukai atau mengharapkan modifikasi semacam itu. Namun, jika hasil akhir terdengar menarik, ganti saja myArraydengan Array.prototype.

Sekarang, itu hanya satu metode dan ada banyak cara untuk mengubah konten array. Kami mungkin membutuhkan sesuatu yang lebih komprehensif ...

2. Buat array observasi kustom

Daripada mengganti metode, Anda bisa membuat array yang dapat diamati sendiri. Ini khususnya salinan implementasi array menjadi baru array seperti objek dan menyediakan kustom push(), pop(), shift(), unshift(), slice(), dan splice()metode serta accesor indeks kustom (asalkan ukuran array hanya diubah melalui salah satu metode tersebut atau lengthproperti).

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

})();

Lihat referensi.Object.defineProperty()

Itu membuat kita lebih dekat tetapi masih belum anti peluru ... yang membawa kita ke:

3. Proksi

Proxy menawarkan solusi lain ... memungkinkan Anda untuk mencegat panggilan metode, pengakses, dll. Yang terpenting, Anda dapat melakukan ini bahkan tanpa memberikan nama properti eksplisit ... yang akan memungkinkan Anda menguji akses berbasis indeks / arbitrer tugas. Anda bahkan dapat mencegah penghapusan properti. Proksi secara efektif memungkinkan Anda untuk memeriksa perubahan sebelumnya memutuskan untuk mengizinkannya ... selain menangani perubahan setelah fakta.

Berikut contoh yang telah dipreteli:

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

})();

kanon
sumber
Terima kasih! Itu berfungsi untuk metode array biasa.
Adakah
4
Saya kira Anda dapat menerapkan metode set(index)dalam prototipe Array dan melakukan sesuatu seperti antisanity mengatakan
Pablo Fernandez
8
Akan jauh lebih baik untuk membuat subclass Array. Biasanya bukan ide yang baik untuk memodifikasi prototipe Array.
Wayne
1
Jawaban yang luar biasa di sini. Kelas ObservableArray sangat bagus. +1
dooburt
1
"'_array.length === 0 && delete _self [index];" - bisakah kamu menjelaskan baris ini?
splintor
23

Dari membaca semua jawaban di sini, saya telah mengumpulkan solusi sederhana yang tidak memerlukan perpustakaan eksternal.

Ini juga menggambarkan dengan lebih baik gagasan umum untuk pendekatan tersebut:

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

var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments);  processQ();};
Sych
sumber
Ini ide yang bagus, tapi bukankah menurut Anda jika misalnya saya ingin mengimplementasikan ini dalam bagan js data array, dan saya memiliki 50 grafik yang berarti 50 array dan setiap array akan diperbarui setiap detik -> bayangkan ukuran array 'myEventsQ' di penghujung hari! Saya pikir kapan perlu menggesernya sesekali
Yahya
2
Anda tidak mengerti solusinya. myEventsQ ADALAH array (salah satu dari 50 array Anda). Potongan ini tidak mengubah ukuran larik, dan tidak menambahkan larik tambahan apa pun, ini hanya mengubah prototipe larik yang sudah ada.
Sych
1
mmmm begitu, lebih banyak penjelasan seharusnya diberikan!
Yahya
3
pushmengembalikan lengtharray. Jadi, Anda bisa mendapatkan nilai yang dikembalikan oleh Array.prototype.push.applyke variabel dan mengembalikannya dari pushfungsi khusus .
adiga
12

Saya menemukan yang berikut ini yang tampaknya mencapai ini: https://github.com/mennovanslooten/Observable-Arrays

Observable-Arays memanjang garis bawah dan dapat digunakan sebagai berikut: (dari halaman itu)

// 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');
});
pengguna1029744
sumber
13
Ini bagus, tapi ada peringatan penting: ketika sebuah array dimodifikasi seperti arr[2] = "foo", notifikasi perubahannya tidak sinkron . Karena JS tidak menyediakan cara apa pun untuk melihat perubahan tersebut, pustaka ini bergantung pada waktu tunggu yang berjalan setiap 250 md dan memeriksa untuk melihat apakah larik telah berubah sama sekali - jadi Anda tidak akan mendapatkan pemberitahuan perubahan sampai berikutnya. waktu waktu habis. Namun, perubahan lain seperti push()mendapat pemberitahuan segera (serentak).
peterflynn
6
Juga saya kira 250 interval akan mempengaruhi kinerja situs Anda jika arraynya besar.
Tomáš Zato - Kembalikan Monica
Baru saja menggunakan ini, berfungsi seperti pesona. Untuk teman-teman kami yang berbasis node, saya menggunakan mantra ini dengan sebuah janji. (Format di komentar sangat merepotkan ...) _ = membutuhkan ('lodash'); membutuhkan ("underscore-amati") ( ); Janji = membutuhkan ("bluebird"); return new Promise (function (selesaikan, tolak) {return _.observe (antrian, 'delete', function () {if ( .isEmpty (queue)) {return resol (action);}});});
Leif
5

Saya menggunakan kode berikut untuk mendengarkan perubahan pada array.

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

Semoga bermanfaat :)

Nadir Laskar
sumber
5

Solusi metode Override push yang paling banyak disukai oleh @canon memiliki beberapa efek samping yang tidak nyaman dalam kasus saya:

  • Itu membuat deskriptor properti push berbeda ( writabledan configurableharus disetel truesebagai ganti false), yang menyebabkan pengecualian di kemudian hari.

  • Itu memunculkan acara beberapa kali ketika push()dipanggil sekali dengan banyak argumen (seperti myArray.push("a", "b")), yang dalam kasus saya tidak perlu dan buruk untuk kinerja.

Jadi ini adalah solusi terbaik yang dapat saya temukan yang memperbaiki masalah sebelumnya dan menurut saya lebih bersih / sederhana / lebih mudah dipahami.

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

Silakan lihat komentar untuk sumber saya dan untuk petunjuk tentang cara mengimplementasikan fungsi mutasi lainnya selain push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.

cprcrack
sumber
@canon Saya memiliki Proksi yang tersedia, tetapi saya tidak dapat menggunakannya karena lariknya dimodifikasi secara eksternal, dan saya tidak dapat memikirkan cara apa pun untuk memaksa penelepon eksternal (yang selain berubah dari waktu ke waktu tanpa kendali saya) untuk menggunakan Proksi .
cprcrack
@canon dan ngomong-ngomong, komentar Anda membuat saya membuat asumsi yang salah, yaitu saya menggunakan operator penyebaran, padahal sebenarnya tidak. Jadi tidak, saya sama sekali tidak memanfaatkan operator penyebaran. Yang saya gunakan adalah parameter rest yang memiliki ...sintaks yang mirip , dan yang dapat dengan mudah diganti dengan penggunaan argumentskata kunci.
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
sumber
1
Sepertinya Object.observe()dan Array.observe()telah ditarik dari spesifikasi. Dukungan telah ditarik dari Chrome. : /
kanon
0

Tidak yakin apakah ini benar-benar mencakup semuanya, tetapi saya menggunakan sesuatu seperti ini (terutama saat debugging) untuk mendeteksi ketika sebuah array memiliki elemen yang ditambahkan:

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;
    }
});
pengguna3337629
sumber
-1

Perpustakaan koleksi yang menarik adalah https://github.com/mgesmundo/smart-collection . Memungkinkan Anda untuk menonton array dan menambahkan tampilan padanya juga. Tidak yakin dengan kinerjanya saat saya mengujinya sendiri. Akan segera memperbarui posting ini.

kontinuity
sumber
-1

Saya bermain-main dan menemukan ini. Idenya adalah bahwa objek tersebut memiliki semua metode Array.prototype yang ditentukan, tetapi mengeksekusinya pada objek array yang terpisah. Ini memberikan kemampuan untuk mengamati metode seperti shift (), pop () dll. Meskipun beberapa metode seperti concat () tidak akan mengembalikan objek OArray. Membebani metode tersebut tidak akan membuat objek dapat diamati jika pengakses digunakan. Untuk mencapai yang terakhir, pengakses ditentukan untuk setiap indeks dalam kapasitas yang diberikan.

Dari segi kinerja ... OArray sekitar 10-25 kali lebih lambat dibandingkan dengan objek Array biasa. Untuk kapasitas pada rentang 1 - 100 selisihnya 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
sumber
Meskipun berfungsi pada elemen yang sudah ada, ini tidak berfungsi ketika elemen ditambahkan dengan array [new_index] = value. Hanya proxy yang bisa melakukannya.
mpm
-5

Saya tidak akan merekomendasikan Anda untuk memperluas prototipe asli. Sebagai gantinya, Anda bisa menggunakan perpustakaan seperti daftar baru; https://github.com/azer/new-list

Ini menciptakan array JavaScript asli dan memungkinkan Anda berlangganan perubahan apa pun. Ini mengumpulkan pembaruan dan memberi Anda perbedaan akhir;

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
sumber