【问题标题】:How to watch for array changes?如何观察数组变化?
【发布时间】:2011-07-03 06:58:45
【问题描述】:

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

我知道 SpiderMonkey 中的 watch() 功能,但这仅在整个变量设置为其他值时才有效。

【问题讨论】:

    标签: javascript


    【解决方案1】:

    有几个选项...

    1。覆盖推送方法

    走快速而肮脏的路线,您可以为您的数组覆盖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()。不过要小心;您环境中的其他代码可能不喜欢或不期望这种修改。不过,如果包罗万象听起来很吸引人,只需将 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.@987654321@

    这让我们更接近,但它仍然不是防弹的......这让我们:

    3。代理

    Proxy 对象为the modern browser 提供了另一种解决方案。它允许您拦截方法调用、访问器等。最重要的是,您甚至可以在不提供显式属性名称的情况下执行此操作……这将允许您测试任意的、基于索引的访问/分配。您甚至可以拦截属性删除。代理将有效地允许您在决定允许更改之前检查更改...除了事后处理更改。

    这是一个精简的示例:

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

    【讨论】:

    • 谢谢!这适用于常规数组方法。关于如何为 "arr[2] = "foo" 之类的事件引发事件的任何想法?
    • 我猜你可以在 Array 的原型中实现一个方法 set(index) 并执行类似 antisanity 所说的操作
    • 子类化 Array 会好很多。修改 Array 的原型通常不是一个好主意。
    • 这里的出色答案。 ObservableArray 的类非常好。 +1
    • "'_array.length === 0 && delete _self[index];" - 你能解释一下这条线吗?
    【解决方案2】:

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

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

    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”数组的大小!我认为什么时候需要时不时地改变它
    • 您不了解解决方案。 myEventsQ 是数组(您的 50 个数组之一)。这个 sn-p 不会改变数组的大小,也不会添加任何额外的数组,它只会改变现有数组的原型。
    • mmmm 我明白了,不过应该提供更多解释!
    • push 返回数组的length。因此,您可以将Array.prototype.push.apply 返回的值获取到一个变量中,并从自定义的push 函数中返回。
    【解决方案3】:

    我使用下面的代码来监听数组的变化。

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

    希望这很有用:)

    【讨论】:

    • 非常有用!谢谢。
    【解决方案4】:

    我发现以下似乎可以做到这一点: 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[2] = "foo" 这样修改数组时,更改通知是异步的。由于 JS 没有提供任何方法来监视此类更改,因此该库依赖于每 250 毫秒运行一次的超时,并检查数组是否发生了变化——所以直到下一次你才会收到更改通知超时运行的时间。但是,像 push() 这样的其他更改会立即(同步)得到通知。
    • 如果数组很大,我猜 250 间隔会影响您的网站性能。
    • 刚刚用过,效果很好。对于我们基于节点的朋友,我使用了这个带有承诺的咒语。(cmets 中的格式很痛苦......) _ = require('lodash');要求(“下划线观察”)();承诺 = 要求(“蓝鸟”); return new Promise(function (resolve, reject) { return _.observe(queue, 'delete', function() { if (.isEmpty(queue)) { return resolve(action); } }); } );
    【解决方案5】:

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

    • 它使推送属性描述符不同(writableconfigurable 应设置为true 而不是false),这会导致稍后出现异常。

    • 当使用多个参数(例如 myArray.push("a", "b"))调用一次 push() 时,它会引发多次事件,在我的情况下这是不必要的,而且对性能不利。

    所以这是我能找到的解决之前问题的最佳解决方案,并且在我看来更简洁/更简单/更容易理解。

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

    请参阅 cmets 获取我的资料以及有关如何实现除 push 之外的其他变异函数的提示:'pop'、'shift'、'unshift'、'splice'、'sort'、'reverse'。

    【讨论】:

    • @canon 我确实有可用的代理,但我不能使用它们,因为数组是在外部修改的,我想不出任何方法来强制外部调用者(除了不时更改之外)不受我控制)使用代理。
    • @canon 顺便说一句,您的评论让我做出了错误的假设,即我使用的是扩展运算符,而实际上我没有。所以不,我根本没有利用传播运算符。我使用的是具有类似... 语法的rest 参数,并且可以使用arguments 关键字轻松替换。
    【解决方案6】:
    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);
    

    【讨论】:

    • 看起来 Object.observe()Array.observe() 已从规范中删除。 Chrome 已取消支持。 ://
    【解决方案7】:

    不确定这是否涵盖了所有内容,但我使用类似的方法(尤其是在调试时)来检测数组何时添加了元素:

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

    【讨论】:

      【解决方案8】:

      一个有趣的收藏库是https://github.com/mgesmundo/smart-collection。允许您查看数组并向它们添加视图。不确定性能,因为我自己正在测试它。很快就会更新这篇文章。

      【讨论】:

        【解决方案9】:

        我摆弄了一下,想出了这个。这个想法是该对象定义了所有 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 添加元素时它不起作用。只有代理才能做到这一点。
        【解决方案10】:

        我不建议您扩展原生原型。相反,您可以使用类似 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]
        
        })
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2021-05-04
          • 1970-01-01
          • 1970-01-01
          • 2017-11-16
          • 2018-07-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多