Làm thế nào để theo dõi các thay đổi của mảng?


106

Trong Javascript, có cách nào để được thông báo khi một mảng được sửa đổi bằng cách sử dụng push, pop, shift hoặc index dựa trên gán không? Tôi muốn một cái gì đó có thể gây ra một sự kiện mà tôi có thể xử lý.

Tôi biết về watch()chức năng trong SpiderMonkey, nhưng điều đó chỉ hoạt động khi toàn bộ biến được đặt thành thứ khác.

Câu trả lời:


169

Có một vài lựa chọn...

1. Ghi đè phương thức đẩy

Đi theo con đường nhanh chóng và bẩn thỉu, bạn có thể ghi đè push()phương thức cho mảng 1 của mình :

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 Ngoài ra, nếu bạn muốn nhắm mục tiêu tất cả các mảng, bạn có thể ghi đè Array.prototype.push(). Hãy thận trọng, mặc dù vậy; mã khác trong môi trường của bạn có thể không thích hoặc không mong đợi loại sửa đổi đó. Tuy nhiên, nếu một bản tóm tắt nghe có vẻ hấp dẫn, chỉ cần thay thế myArraybằng Array.prototype.

Bây giờ, đó chỉ là một phương pháp và có rất nhiều cách để thay đổi nội dung mảng. Chúng ta có lẽ cần một cái gì đó toàn diện hơn ...

2. Tạo một mảng có thể quan sát tùy chỉnh

Thay vì ghi đè các phương thức, bạn có thể tạo mảng có thể quan sát của riêng mình. Đây thực hiện cụ thể bản một mảng thành một mảng mới giống như đối tượng và cung cấp tùy chỉnh push(), pop(), shift(), unshift(), slice(), và splice()các phương pháp cũng như accessors chỉ số tùy chỉnh (với điều kiện là kích thước mảng chỉ được sửa đổi thông qua một trong những phương pháp nói trên hoặc lengthtài sản).

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

})();

Xem để tham khảo.Object.defineProperty()

Điều đó đưa chúng ta đến gần hơn nhưng nó vẫn không phải là vật liệu chống đạn ... đưa chúng ta đến:

3. Proxy

Proxy cung cấp một giải pháp khác ... cho phép bạn chặn các cuộc gọi phương thức, trình truy cập, v.v. Quan trọng nhất, bạn có thể làm điều này mà không cần cung cấp tên thuộc tính rõ ràng ... cho phép bạn kiểm tra quyền truy cập dựa trên chỉ mục / tùy ý sự phân công. Bạn thậm chí có thể chặn việc xóa tài sản. Proxy sẽ cho phép bạn kiểm tra thay đổi một cách hiệu quả trước khi quyết định cho phép nó ... ngoài việc xử lý thay đổi sau thực tế.

Đây là một mẫu đã rút gọn:

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

})();


Cảm ơn! Điều đó hoạt động đối với các phương thức mảng thông thường. Bất kỳ ý tưởng nào về cách nâng cao sự kiện cho một cái gì đó như "arr [2] =" foo "?
Sridatta Thatipamala

4
Tôi đoán bạn có thể thực hiện một phương pháp set(index)trong nguyên mẫu Array Avatar của và làm điều gì đó như antisanity nói
Pablo Fernandez

8
Sẽ tốt hơn nhiều nếu phân lớp Mảng. Nói chung không phải là một ý kiến ​​hay khi sửa đổi nguyên mẫu của Array.
Wayne ngày

1
Câu trả lời nổi bật ở đây. Lớp của ObservableArray là tuyệt vời. +1
dooburt

1
"'_array.length === 0 && delete _self [index];" - bạn có thể giải thích dòng này không?
splintor

23

Từ việc đọc tất cả các câu trả lời ở đây, tôi đã tập hợp một giải pháp đơn giản hóa mà không yêu cầu bất kỳ thư viện bên ngoài nào.

Nó cũng minh họa tốt hơn nhiều ý tưởng chung cho cách tiếp cận:

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

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

Đây là một ý tưởng hay, nhưng bạn có nghĩ rằng nếu chẳng hạn tôi muốn triển khai điều này trong mảng dữ liệu js biểu đồ và tôi có 50 biểu đồ có nghĩa là 50 mảng và mỗi mảng sẽ được cập nhật mỗi giây -> hãy tưởng tượng kích thước của mảng 'myEventsQ' vào cuối ngày! Tôi nghĩ khi nào cần phải thay đổi nó mọi lúc mọi nơi
Yahya

2
Bạn không hiểu giải pháp. myEventsQ LÀ mảng (một trong 50 mảng của bạn). Đoạn mã này không thay đổi kích thước của mảng và không thêm bất kỳ mảng bổ sung nào, nó chỉ thay đổi nguyên mẫu của những mảng hiện có.
Sych

1
mmmm tôi hiểu rồi, mặc dù vậy, cần phải cung cấp thêm giải thích!
Yahya

3
pushtrả về lengthmảng. Vì vậy, bạn có thể lấy giá trị được trả về bởi Array.prototype.push.applymột biến và trả về từ pushhàm tùy chỉnh .
adiga 14/07/19

12

Tôi tìm thấy phần sau dường như thực hiện được điều này: https://github.com/mennovanslooten/Observable-Arrays

Mảng có thể quan sát mở rộng dấu gạch dưới và có thể được sử dụng như sau: (từ trang đó)

// 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
Điều này thật tuyệt, nhưng có một lưu ý quan trọng: khi một mảng được sửa đổi như thế nào arr[2] = "foo", thông báo thay đổi sẽ không đồng bộ . Vì JS không cung cấp bất kỳ cách nào để theo dõi những thay đổi như vậy, nên thư viện này dựa vào thời gian chờ chạy sau mỗi 250 mili giây và kiểm tra xem liệu mảng có thay đổi gì không - vì vậy bạn sẽ không nhận được thông báo thay đổi cho đến lần tiếp theo thời gian hết giờ chạy. push()Tuy nhiên, các thay đổi khác như được thông báo ngay lập tức (đồng bộ).
peterflynn

6
Ngoài ra, tôi đoán khoảng thời gian 250 sẽ ảnh hưởng đến hiệu suất trang web của bạn nếu mảng lớn.
Tomáš Zato - Phục hồi Monica

Chỉ cần sử dụng này, hoạt động như một sự quyến rũ. Đối với những người bạn dựa trên nút của chúng tôi, tôi đã sử dụng câu thần chú này với một lời hứa. (Định dạng trong nhận xét là một nỗi đau ...) _ = request ('lodash'); request ("gạch dưới-quan sát") ( ); Promise = request ("bluebird"); return new Promise (function (giải quyết, từ chối) {return _.observe (queue, 'delete', function () {if ( .isEmpty (queue)) {return Resolution (action);}});});
Leif

5

Tôi đã sử dụng đoạn mã sau để lắng nghe các thay đổi đối với một mảng.

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

Hy vọng điều này hữu ích :)


5

Giải pháp phương pháp đẩy Ghi đè được ủng hộ nhiều nhất bởi @canon có một số tác dụng phụ gây bất tiện trong trường hợp của tôi:

  • Nó làm cho bộ mô tả thuộc tính đẩy trở nên khác biệt ( writableconfigurablenên được đặt truethay vì false), điều này gây ra các ngoại lệ ở điểm sau.

  • Nó làm tăng sự kiện nhiều lần khi push()được gọi một lần với nhiều đối số (chẳng hạn như myArray.push("a", "b")), trong trường hợp của tôi là không cần thiết và không tốt cho hiệu suất.

Vì vậy, đây là giải pháp tốt nhất mà tôi có thể tìm thấy để khắc phục các vấn đề trước đó và theo ý kiến ​​của tôi là gọn gàng hơn / đơn giản hơn / dễ hiểu hơn.

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

Vui lòng xem nhận xét cho các nguồn của tôi và để biết gợi ý về cách triển khai các hàm đột biến khác ngoài push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.


@canon Tôi có sẵn proxy, nhưng tôi không thể sử dụng chúng vì mảng được sửa đổi bên ngoài và tôi không thể nghĩ ra bất kỳ cách nào để buộc người gọi bên ngoài (ngoài việc thay đổi theo thời gian mà tôi không kiểm soát) sử dụng Proxy .
cprcrack

@canon và nhân tiện, nhận xét của bạn đã khiến tôi đưa ra một giả định sai lầm, đó là tôi đang sử dụng toán tử spread, trong khi thực tế thì không. Vì vậy, không, tôi không tận dụng toán tử spread. Những gì tôi đang sử dụng là tham số còn lại có ...cú pháp tương tự và có thể dễ dàng thay thế bằng cách sử dụng argumentstừ khóa.
cprcrack 14/12/18


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
Có vẻ như Object.observe()Array.observe()đã được rút khỏi thông số kỹ thuật. Hỗ trợ đã được rút khỏi Chrome. : /
canon

0

Không chắc liệu điều này có bao gồm hoàn toàn mọi thứ hay không, nhưng tôi sử dụng một cái gì đó như thế này (đặc biệt là khi gỡ lỗi) để phát hiện khi một mảng có một phần tử được thêm vào:

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

Tôi đã tìm kiếm xung quanh và nghĩ ra điều này. Ý tưởng là đối tượng có tất cả các phương thức Array.prototype được định nghĩa, nhưng thực thi chúng trên một đối tượng mảng riêng biệt. Điều này cung cấp khả năng quan sát các phương thức như shift (), pop (), v.v. Mặc dù một số phương thức như concat () sẽ không trả về đối tượng OArray. Việc ghi đè các phương thức đó sẽ không làm cho đối tượng có thể quan sát được nếu người truy cập được sử dụng. Để đạt được điều sau này, người truy cập được xác định cho mỗi chỉ mục trong khả năng nhất định.

Hiệu suất khôn ngoan ... OArray chậm hơn khoảng 10-25 lần so với đối tượng Array thông thường. Đối với định suất trong phạm vi 1 - 100, sự khác biệt là 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;
    }
}

Mặc dù nó hoạt động trên các phần tử hiện có, nhưng nó không hoạt động khi một phần tử được thêm vào mảng [new_index] = value. Chỉ proxy mới có thể làm được điều đó.
mpm

-5

Tôi không khuyên bạn nên mở rộng các nguyên mẫu gốc. Thay vào đó, bạn có thể sử dụng một thư viện như new-list; https://github.com/azer/new-list

Nó tạo ra một mảng JavaScript gốc và cho phép bạn đăng ký bất kỳ thay đổi nào. Nó cập nhật hàng loạt và cung cấp cho bạn sự khác biệt cuối cùng;

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]

})
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.