【发布时间】:2011-07-03 06:58:45
【问题描述】:
在 Javascript 中,有没有办法在使用 push、pop、shift 或基于索引的赋值修改数组时得到通知?我想要一些可以触发我可以处理的事件的东西。
我知道 SpiderMonkey 中的 watch() 功能,但这仅在整个变量设置为其他值时才有效。
【问题讨论】:
标签: javascript
在 Javascript 中,有没有办法在使用 push、pop、shift 或基于索引的赋值修改数组时得到通知?我想要一些可以触发我可以处理的事件的东西。
我知道 SpiderMonkey 中的 watch() 功能,但这仅在整个变量设置为其他值时才有效。
【问题讨论】:
标签: javascript
有几个选项...
走快速而肮脏的路线,您可以为您的数组覆盖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。
现在,这只是一种方法,有很多方法可以更改数组内容。我们可能需要更全面的东西...
您可以创建自己的可观察数组,而不是覆盖方法。这个特定的实现将一个数组复制到一个新的类数组对象中,并提供自定义的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@。
这让我们更接近,但它仍然不是防弹的......这让我们:
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);
})();
【讨论】:
set(index) 并执行类似 antisanity 所说的操作
通过阅读此处的所有答案,我已经组装了一个不需要任何外部库的简化解决方案。
它还更好地说明了该方法的总体思路:
function processQ() {
// ... this will be called on each .push
}
var myEventsQ = [];
myEventsQ.push = function() { Array.prototype.push.apply(this, arguments); processQ();};
【讨论】:
push 返回数组的length。因此,您可以将Array.prototype.push.apply 返回的值获取到一个变量中,并从自定义的push 函数中返回。
我使用下面的代码来监听数组的变化。
/* @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;
}
});
}
希望这很有用:)
【讨论】:
我发现以下似乎可以做到这一点: 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() 这样的其他更改会立即(同步)得到通知。
@canon 最受好评的 Override push method 解决方案有一些对我来说不方便的副作用:
它使推送属性描述符不同(writable 和configurable 应设置为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'。
【讨论】:
... 语法的rest 参数,并且可以使用arguments 关键字轻松替换。
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 已取消支持。 ://
不确定这是否涵盖了所有内容,但我使用类似的方法(尤其是在调试时)来检测数组何时添加了元素:
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;
}
});
【讨论】:
一个有趣的收藏库是https://github.com/mgesmundo/smart-collection。允许您查看数组并向它们添加视图。不确定性能,因为我自己正在测试它。很快就会更新这篇文章。
【讨论】:
我摆弄了一下,想出了这个。这个想法是该对象定义了所有 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;
}
}
【讨论】:
我不建议您扩展原生原型。相反,您可以使用类似 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]
})
【讨论】: