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

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

watch()SpiderMonkey 의 기능에 대해 알고 있지만 전체 변수가 다른 것으로 설정된 경우에만 작동합니다.



답변

몇 가지 옵션이 있습니다 …

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

})();


답변

여기에서 모든 답변을 읽음으로써 외부 라이브러리가 필요하지 않은 단순화 된 솔루션을 모았습니다.

또한 접근 방식에 대한 일반적인 아이디어를 훨씬 더 잘 보여줍니다.

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

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


답변

이 작업을 수행하는 것으로 보이는 다음을 발견했습니다.
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');
});


답변

다음 코드를 사용하여 배열 변경 사항을 수신했습니다.

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

이것이 유용했기를 바랍니다. 🙂


답변

@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’.


답변

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


답변

이것이 절대적으로 모든 것을 포함하는지 확실하지 않지만, 배열에 요소가 추가 된 경우를 감지하기 위해 다음과 같은 것을 사용합니다 (특히 디버깅 할 때).

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