如何观察数组变化?

IT技术 javascript
2021-01-22 09:40:13

在 Javascript 中,有没有办法在使用 push、pop、shift 或基于索引的赋值修改数组时收到通知?我想要一些可以触发我可以处理的事件的东西。

我知道watch()SpiderMonkey 中功能,但只有在整个变量设置为其他内容时才有效。

6个回答

有几个选项...

1.覆盖push方法

走快速而肮脏的路线,您可以覆盖push()数组1方法

Object.defineProperty(myArray, "push", {
  // hide from for..in and prevent further overrides (via default descriptor values)
  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(). 不过要小心;您环境中的其他代码可能不喜欢或不希望进行这种修改。尽管如此,如果一个包罗万象的内容听起来很吸引人,只需将其替换myArrayArray.prototype.

现在,这只是一种方法,有很多方法可以更改数组内容。我们可能需要更全面的东西......

2.创建自定义的observable数组

您可以创建自己的可观察数组,而不是覆盖方法。这个特定的实现将一个数组复制到一个新的类似数组的对象中,并提供自定义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);

})();

优秀的答案在这里。ObservableArray 的类非常好。+1
2021-03-15 09:40:13
我猜你可以set(index)在 Array 的原型中实现一个方法并做一些像 antisanity 说的
2021-03-17 09:40:13
将 Array 子类化会好得多。修改 Array 的原型通常不是一个好主意。
2021-03-30 09:40:13
谢谢!这适用于常规数组方法。关于如何为诸如 "arr[2] = "foo" 之类的事件引发事件的任何想法?
2021-04-03 09:40:13
"'_array.length === 0 && 删除 _self[index];" - 你能解释一下这条线吗?
2021-04-06 09:40:13

通过阅读此处的所有答案,我组装了一个不需要任何外部库的简化解决方案。

它还更好地说明了该方法的总体思路:

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”数组!我想什么时候需要时不时地改变它
2021-03-23 09:40:13
push返回length数组的 。因此,您可以获取 Array.prototype.push.apply变量返回的值并从自定义push函数返回它
2021-04-03 09:40:13
你不明白解决方案。myEventsQ 是数组(您的 50 个数组之一)。这个片段没有改变数组的大小,也没有添加任何额外的数组,它只是改变了现有数组的原型。
2021-04-07 09:40:13
嗯,我明白了,不过应该提供更多解释!
2021-04-10 09:40:13

我使用以下代码来监听对数组的更改。

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

希望这是有用的:)

很有用!谢谢。
2021-03-26 09:40:13

我发现以下似乎可以完成此操作: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');
});
刚刚使用了这个,就像一个魅力。对于我们基于节点的朋友,我使用了这个带有Promise的咒语。(注释中的格式很痛苦......)_ = require('lodash'); 要求(“下划线观察”)();Promise = require("bluebird"); return new Promise(function (resolve, reject) { return _.observe(queue, 'delete', function() { if ( .isEmpty(queue)) { return resolve(action); } }); });
2021-03-21 09:40:13
这很好,但有一个重要的警告:当数组被修改时arr[2] = "foo",更改通知是异步的由于 JS 没有提供任何方法来监视此类更改,因此该库依赖于每 250 毫秒运行一次的超时并检查数组是否发生了更改——因此您在下一次之前不会收到更改通知超时运行的时间。但是,其他更改如push()立即(同步)得到通知。
2021-03-26 09:40:13
此外,如果阵列很大,我猜 250 间隔会影响您的站点性能。
2021-03-27 09:40:13

@canon 最受好评的Override push 方法解决方案有一些副作用,对我来说很不方便:

  • 它使 push 属性描述符不同(writable并且configurable应该设置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
    }
});

请参阅我的来源的评论以及关于如何实现除 push 之外的其他变异函数的提示:'pop'、'shift'、'unshift'、'splice'、'sort'、'reverse'。

2021-03-13 09:40:13
@canon 我确实有可用的代理,但我无法使用它们,因为数组是在外部修改的,而且我想不出任何方法来强制外部调用者(除了在不受我控制的情况下不时更改)使用代理.
2021-03-25 09:40:13
@canon 顺便说一句,你的评论让我做出了一个错误的假设,那就是我正在使用传播运算符,而实际上我没有。所以不,我根本没有利用价差运算符。我使用的是 rest 参数,它具有类似的...语法,并且可以使用arguments关键字轻松替换
2021-04-04 09:40:13