어레이 변경을 관찰하는 방법은 무엇입니까?


106

Javascript에서 푸시, 팝, 시프트 또는 인덱스 기반 할당을 사용하여 배열이 수정 될 때 알림을받는 방법이 있습니까? 내가 처리 할 수있는 이벤트를 발생시킬 무언가를 원합니다.

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

})();


감사! 그것은 일반 배열 방법에서 작동합니다. 같은 뭔가 이벤트를 발생하는 방법에 어떤 아이디어 "편곡 [2] ="foo는 "?
Sridatta Thatipamala

4
난 당신이 방법을 구현할 수 추측 set(index)배열의 프로토 타입 및 antisanity 같은 것을 말한다 할
파블로 페르난데스

8
Array를 하위 클래스로 만드는 것이 훨씬 낫습니다. 일반적으로 Array의 프로토 타입을 수정하는 것은 좋은 생각이 아닙니다.
Wayne

1
여기에 뛰어난 대답. ObservableArray의 클래스는 훌륭합니다. 한
dooburt

1
" '_array.length === 0 && _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 개 배열 중 하나)입니다. 이 스 니펫은 배열의 크기를 변경하지 않으며 추가 배열을 추가하지 않으며 기존 배열의 프로토 타입 만 변경합니다.
Sych

1
음, 그래도 더 많은 설명이 제공되어야합니다!
야야

3
pushlength배열 의 를 반환합니다 . 따라서에서 반환 된 값 Array.prototype.push.apply을 변수로 가져 와서 사용자 지정 push함수 에서 반환 할 수 있습니다.
adiga 19

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

13
이것은 훌륭하지만 중요한주의 사항이 있습니다. 배열이.와 같이 수정 arr[2] = "foo"되면 변경 알림이 비동기 적 입니다. JS는 이러한 변경 사항을 감시하는 방법을 제공하지 않기 때문에이 라이브러리는 250ms마다 실행되는 시간 제한에 의존하고 배열이 전혀 변경되었는지 확인하므로 다음 시간까지 변경 알림을받지 못합니다. 시간 제한이 실행됩니다. 그러나 다른 변경 사항은 push()즉시 (동 기적으로) 알림을받습니다.
peterflynn 2013

6
또한 어레이가 크면 250 간격이 사이트 성능에 영향을 미칠 것이라고 생각합니다.
Tomáš Zato-Monica 복원

그냥 이것을 사용하면 매력처럼 작동합니다. 노드 기반 친구들을 위해이 주문을 약속과 함께 사용했습니다. (댓글 형식은 고통 스럽습니다 ...) _ = require ( 'lodash'); require ( "underscore-observe") ( ); Promise = require ( "bluebird"); return new Promise (function (resolve, reject) {return _.observe (queue, 'delete', function () {if ( .isEmpty (queue)) {return resolve (action);}});});
Leif

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

@canon 의 가장 많이 찬성 된 Override push 메서드 솔루션에는 제 경우에는 불편한 몇 가지 부작용이 있습니다.

  • 그것은 푸시 속성 기술자 다른한다 ( writableconfigurable설정해야합니다 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
    }
});

내 소스에 대한 주석과 푸시 외에 다른 돌연변이 기능을 구현하는 방법에 대한 힌트를 참조하십시오 : 'pop', 'shift', 'unshift', 'splice', 'sort', '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

흥미로운 컬렉션 라이브러리는 https://github.com/mgesmundo/smart-collection 입니다. 배열을 관찰하고보기를 추가 할 수도 있습니다. 내가 직접 테스트하고 있기 때문에 성능이 확실하지 않습니다. 곧이 게시물을 업데이트 할 예정입니다.


-1

나는 주위를 돌아 다니며 이것을 생각해 냈습니다. 아이디어는 객체에 모든 Array.prototype 메소드가 정의되어 있지만 별도의 배열 객체에서 실행된다는 것입니다. 이것은 shift (), pop () 등과 같은 메소드를 관찰 할 수있는 기능을 제공합니다. concat ()과 같은 일부 메소드는 OArray 객체를 반환하지 않습니다. 이러한 메서드를 오버로드해도 접근자가 사용되는 경우 개체를 관찰 할 수 없습니다. 후자를 달성하기 위해 주어진 용량 내에서 각 인덱스에 대해 접근자를 정의합니다.

성능면에서 ... OArray는 일반 Array 객체에 비해 약 10-25 배 느립니다. 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;
    }
}

기존 요소에서 작동하지만 array [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]

})
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.