Javascript 如何观察数组变化?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/5100376/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to watch for array changes?
提问by Sridatta Thatipamala
In Javascript, is there a way to be notified when an array is modified using push, pop, shift or index-based assignment? I want something that would fire an event that I could handle.
在 Javascript 中,有没有办法在使用 push、pop、shift 或基于索引的赋值修改数组时收到通知?我想要一些可以触发我可以处理的事件的东西。
I know about the watch()
functionality in SpiderMonkey, but that only works when the entire variable is set to something else.
我知道watch()
SpiderMonkey 中的功能,但只有当整个变量设置为其他内容时才有效。
回答by canon
There are a few options...
有几个选项...
1. Override the push method
1.覆盖push方法
Going the quick and dirty route, you could override the push()
method for your array1:
走快速而肮脏的路线,您可以覆盖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;
}
});
1Alternatively, if you'd like to target allarrays, you could override Array.prototype.push()
. Use caution, though; other code in your environment may not like or expect that kind of modification. Still, if a catch-all sounds appealing, just replace myArray
with Array.prototype
.
1或者,如果您想针对所有数组,您可以覆盖Array.prototype.push()
. 不过要小心;您环境中的其他代码可能不喜欢或不希望进行这种修改。尽管如此,如果一个包罗万象的内容听起来很吸引人,只需将其替换myArray
为Array.prototype
.
Now, that's just one method and there are lots of ways to change array content. We probably need something more comprehensive...
现在,这只是一种方法,有很多方法可以更改数组内容。我们可能需要更全面的东西......
2. Create a custom observable array
2.创建自定义的observable数组
Rather than overriding methods, you could create your own observable array. This particular implementation copies an array into a new array-like object and provides custom push()
, pop()
, shift()
, unshift()
, slice()
, and splice()
methods as well ascustom index accessors (provided that the array size is only modified via one of the aforementioned methods or the length
property).
您可以创建自己的可观察数组,而不是覆盖方法。这个特定的实现将一个数组复制到一个新的类似数组的对象中,并提供自定义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());
})();
See Object.defineProperty()
for reference.
请参阅以供参考。Object.defineProperty()
That gets us closer but it's still not bullet proof... which brings us to:
这让我们更接近,但它仍然不是防弹...这让我们:
3. Proxies
3. 代理
Proxiesoffer another solution... allowing you to intercept method calls, accessors, etc. Most importantly, you can do this without even providing an explicit property name... which would allow you to test for an arbitrary, index-based access/assignment. You can even intercept property deletion. Proxies would effectively allow you to inspect a change beforedeciding to allow it... in addition to handling the change after the fact.
代理提供了另一种解决方案...允许您拦截方法调用、访问器等。最重要的是,您甚至可以在不提供显式属性名称的情况下执行此操作...这将允许您测试任意的、基于索引的访问/任务。您甚至可以拦截属性删除。代理将有效地允许您在决定允许之前检查更改......除了在事后处理更改之外。
Here's a stripped down sample:
这是一个精简的样本:
(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);
})();
回答by Sych
From reading all the answers here, I have assembled a simplified solution that does not require any external libraries.
通过阅读此处的所有答案,我组装了一个不需要任何外部库的简化解决方案。
It also illustrates much better the general idea for the approach:
它还更好地说明了该方法的总体思路:
function processQ() {
// ... this will be called on each .push
}
var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments); processQ();};
回答by user1029744
I found the following which seems to accomplish this: https://github.com/mennovanslooten/Observable-Arrays
我发现以下似乎可以完成此操作:https: //github.com/mennovanslooten/Observable-Arrays
Observable-Arrays extends underscore and can be used as follow: (from that page)
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');
});
回答by Nadir Laskar
I used the following code to listen to changes to an array.
我使用以下代码来监听对数组的更改。
/* @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;
}
});
}
Hope this was useful :)
希望这是有用的:)
回答by cprcrack
The most upvoted Override push methodsolution by @canon has some side-effects that were inconvenient in my case:
@canon 最受好评的Override push 方法解决方案有一些副作用,对我来说很不方便:
It makes the push property descriptor different (
writable
andconfigurable
should be settrue
instead offalse
), which causes exceptions in a later point.It raises the event multiple times when
push()
is called once with multiple arguments (such asmyArray.push("a", "b")
), which in my case was unnecessary and bad for performance.
它使 push 属性描述符不同(
writable
并且configurable
应该设置true
而不是false
),这会导致稍后出现异常。当
push()
使用多个参数(例如myArray.push("a", "b")
)调用一次时,它会多次引发事件,这在我的情况下是不必要的并且对性能不利。
So this is the best solution I could find that fixes the previous issues and is in my opinion cleaner/simpler/easier to understand.
所以这是我能找到的最好的解决方案,它解决了以前的问题,在我看来更清晰/更简单/更容易理解。
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
}
});
Please see comments for my sources and for hints on how to implement the other mutating functions apart from push: 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'.
请参阅我的来源的评论以及关于如何实现除 push 之外的其他变异函数的提示:'pop'、'shift'、'unshift'、'splice'、'sort'、'reverse'。
回答by user3337629
Not sure if this covers absolutely everything, but I use something like this (especially when debugging) to detect when an array has an element added:
不确定这是否涵盖了所有内容,但我使用类似的方法(尤其是在调试时)来检测数组何时添加了元素:
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;
}
});
回答by Martin Wantke
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);
回答by kontinuity
An interesting collection library is https://github.com/mgesmundo/smart-collection. Allows you to watch arrays and add views to them as well. Not sure about the performance as I am testing it out myself. Will update this post soon.
一个有趣的收藏库是https://github.com/mgesmundo/smart-collection。允许您查看数组并向它们添加视图。不确定性能,因为我自己正在测试它。很快就会更新这个帖子。
回答by sysaxis
I fiddled around and came up with this. The idea is that the object has all the Array.prototype methods defined, but executes them on a separate array object. This gives the ability to observe methods like shift(), pop() etc. Although some methods like concat() won't return the OArray object. Overloading those methods won't make the object observable if accessors are used. To achieve the latter, the accessors are defined for each index within given capacity.
我摆弄了一下,想出了这个。这个想法是该对象定义了所有 Array.prototype 方法,但在单独的数组对象上执行它们。这提供了观察 shift()、pop() 等方法的能力。尽管 concat() 等一些方法不会返回 OArray 对象。如果使用访问器,重载这些方法不会使对象变得可观察。为了实现后者,在给定容量内为每个索引定义访问器。
Performance wise... OArray is around 10-25 times slower compared to the plain Array object. For the capasity in a range 1 - 100 the difference is 1x-3x.
性能方面...与普通 Array 对象相比,OArray 大约慢 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;
}
}
回答by Azer
I wouldn't recommend you to extend native prototypes. Instead, you can use a library like new-list; https://github.com/azer/new-list
我不建议你扩展原生原型。相反,您可以使用像 new-list 这样的库;https://github.com/azer/new-list
It creates a native JavaScript array and lets you subscribe to any change. It batches the updates and gives you the final diff;
它创建了一个原生 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]
})