Reputation: 283
Is there a way to attach a listener to an array that is being built by another process so that when array.length reaches certain value, the listener executes a callback and fires appropriate code?
fooArray = [];
// another code adds objects to array
if (fooArray.length = 5) {
// callback
} else {
// continue checking fooArray.length
}
Upvotes: 3
Views: 628
Reputation: 27282
Well, yes and no. There's no way to do it in a 100% safe way, because JavaScript doesn't support operator overloading, meaning you can't modify the behavior of the array indexing operator ([]
).
However, it's rare that arrays are added to with the array indexing operator. That is, this is a very uncommon (but valid) way to add items to an array:
var arr = [];
for( i=0; i<5; i++ ) arr[i] = i;
The result will be an array with five elements. More commonly, though, you see this:
var arr = [];
for( i=0; i<5; i++ ) arr.push( i );
Now that's something we can work with. So now the solution is to wrap the original functionality of the push
function with additional functionality that raises an event.
There are three basic ways to do this. The first is to modify Array.prototype.push
. I would steer you clear of that approach: the chances of causing extremely hard-to-track-down defects is to high. The second is to create your own array type that extends JavaScript's built-in Array
type. This would be the best approach if you'll be creating a lot of these arrays that need listening to. The last approach is just to modify an individual array, which is the least work, and the approach I'll describe here.
You can get as sophisticated as you want with any of these approaches, but I'll just give a pretty minimal foundation, that allows you to attach arbitrary listeners, and could be extended for other functionality (removing listeners, raising events on different array modification methods, etc.).
First, you'll need an array, and a property to hold listeners:
var arr = [];
arr._listeners = [];
Then you can modify the push
method accordingly:
arr.push = function() {
var arr = this;
Array.prototype.push.apply( arr, arguments );
arr._listeners.forEach( function(listener) {
listener( { event: 'push', newLength: arr.length } );
});
}
You can pretty much put anything in that event object you want (like old count, element being added, a timestamp, etc.)
To make this robust, you'll have to use this technique for every other method of Array
that modifies the array, such as pop
, shift
, etc. See all the methods of Array
here:
Depending on your needs, there are additional techniques you could consider. For example, if you do a lot of wrapping of functions (like we did above with push
), you should consider some kind of aspect-oriented programming (AOP) framework like meld. You could pretty easily write your own, too. For example, we could re-work the above thusly:
function after( f, after ) {
return function() {
f.apply( this, arguments );
after.apply( this, arguments );
}
}
var arr = [];
// convenience function for broadcasting an event
arr._broadcast = function( o ) {
this._listeners.forEach( function(listener) {
listener( o );
}
}
arr.push = after( arr.push, function() {
this._broadcast( { event: 'push', newLength: arr.length } );
});
The advantage of that approach is that it makes it easier to extend the other methods of Array
:
arr.pop = after( arr.pop, function() {
this._broadcast( { event: 'pop', newLength: arr.length } );
});
And so forth.
If you need to be able to make many of these "broadcasting" arrays, you could, as previously mentioned, create your own custom array type extending Array
, but JavaScript provides oh so many more flexible patterns for code re-use than traditional OOP. So instead of creating a EventArray
that extends Array
, you could just create a function of Array.prototype
that would magically make that instance an event emitter:
Array.prototype.makeBroadcaster = function() {
this._listeners = this._listeners || [];
this._broadcast = function( o ) {
this._listeners.forEach( function(listener) {
listener( o );
}
this.registerListener = function( listener ) {
this._listeners.push( listener );
}
this.push = after( this.push, function() {
this._broadcast( { event: 'push', newLength: arr.length } );
}
this.pop = after( this.push, function() {
this._broadcast( { event: 'push', newLength: arr.length } );
}
this.shift = after( this.push, function() {
this._broadcast( { event: 'shift', newLength: arr.length } );
}
// etc....
}
Now, whenever you have an array that you want to broadcast what happens to it, all you have to do is this:
var arr = [];
arr.makeBroadcaster();
arr.addListener( function(evt) {
console.log( "%s (new length: %d)", evt.event, evt.newLength );
} );
Some closing notes:
As I mentioned at the start of the post, if your code modifies the array using the array indexing operator, there's no way to raise an event on that. Specifically, arrays can be "grown" using the array indexing operator. In other words, if you array contains 5 elements, and you execute arr[9] = 'hello'
, your array will now have ten elements in it (with 5-8 being undefined
). There's no way to detect this, but it's pretty uncommon that arrays are modified this way.
You can also modify the length of an array with the length
property. That is, if you have a ten-element array, and you call arr.length = 5
, it will truncate the array to have only five elements. It's possible that this could be detected by overriding the length
property (using Object.defineProperty
), but that sounds really dangerous if it's possible at all. And again, truncating an array in this way is pretty uncommon in JavaScript.
If the events really absolutely have to be 100% accurate, even in the cases above, well, your best bet would be able to create your own object that has basic array functions like push
and the like. It will be very awkward, though, because you'll have to implement something like get(index)
instead of using the more convenient array indexing operators.
Here's a jsfiddle demonstrating the whole thing in action: http://jsfiddle.net/g68jC/
Isn't JavaScript neat?
Upvotes: 3
Reputation: 1077
You have to use own methods for changing in array
Array.prototype.myPush = function () {
this.push.apply( this, arguments );
if ( this._binder && this._binder.test.call( this ) ) {
this._binder.callback.call( this );
}
}
Array.prototype.bind = function (tester, callback) {
this._binder = {
test: tester,
callback: callback
};
}
var arr = [4, 6, 9, 20];
arr.bind( function () {
return this.length >= 5;
}, function () {
console.log( "Array has exceeded" );
} );
arr.myPush( 40 );
Upvotes: 3