Prohibited duplicate listeners of the same event
parent
875c43cee4
commit
66e3d5b1ef
|
@ -193,6 +193,12 @@ module.exports = Trait( 'Evented',
|
||||||
else if ( typeof listener !== 'function' ) {
|
else if ( typeof listener !== 'function' ) {
|
||||||
throw TypeError( "Event listener must be a function" );
|
throw TypeError( "Event listener must be a function" );
|
||||||
}
|
}
|
||||||
|
else if ( this._hooksEvent( ev, listener ) )
|
||||||
|
{
|
||||||
|
throw Error(
|
||||||
|
`Listener has already been bound to event \`${ev}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.hookEvent( ev, listener );
|
this.hookEvent( ev, listener );
|
||||||
return this;
|
return this;
|
||||||
|
@ -222,12 +228,17 @@ module.exports = Trait( 'Evented',
|
||||||
let evls = this._events[ ev ],
|
let evls = this._events[ ev ],
|
||||||
avail = this._gaps[ ev ].pop();
|
avail = this._gaps[ ev ].pop();
|
||||||
|
|
||||||
|
if ( listener[ _evid ] === undefined )
|
||||||
|
{
|
||||||
|
listener[ _evid ] = {};
|
||||||
|
}
|
||||||
|
|
||||||
if ( avail === undefined ) {
|
if ( avail === undefined ) {
|
||||||
listener[ _evid ] = evls.length;
|
listener[ _evid ][ ev ] = evls.length;
|
||||||
evls.push( listener );
|
evls.push( listener );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
listener[ _evid ] = avail;
|
listener[ _evid ][ ev ] = avail;
|
||||||
evls[ avail ] = listener;
|
evls[ avail ] = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +246,27 @@ module.exports = Trait( 'Evented',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether LISTENER is actively monitoring event EV
|
||||||
|
*
|
||||||
|
* Note that this method does not verify by reference that the given
|
||||||
|
* listener is legitimate---it assumes that the event metadata attached
|
||||||
|
* to LISTENER (if any) has not been tampered with or forged.
|
||||||
|
*
|
||||||
|
* @O {1} constant time
|
||||||
|
*
|
||||||
|
* @param {string} ev defined event id
|
||||||
|
* @param {Function} listener listener to query
|
||||||
|
*
|
||||||
|
* @return {boolean} whether LISTENER hooks event EV
|
||||||
|
*/
|
||||||
|
_hooksEvent( ev, listener )
|
||||||
|
{
|
||||||
|
let levdata = listener[ _evid ];
|
||||||
|
return ( levdata && ( levdata[ ev ] !== undefined ) );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alias for #on
|
* Alias for #on
|
||||||
*
|
*
|
||||||
|
@ -274,15 +306,18 @@ module.exports = Trait( 'Evented',
|
||||||
throw Error( `Cannot unhook undefined event \`${ev}'` );
|
throw Error( `Cannot unhook undefined event \`${ev}'` );
|
||||||
}
|
}
|
||||||
|
|
||||||
let evls = this._events[ ev ],
|
let evls = this._events[ ev ],
|
||||||
index = listener[ _evid ];
|
levdata = listener[ _evid ] || {},
|
||||||
|
index = levdata[ ev ];
|
||||||
|
|
||||||
// this is important, since we (a) cannot necessarily trust that the
|
// this is important, since we (a) cannot necessarily trust that the
|
||||||
// cached index hasn't been forged and (b) if a listener is removed
|
// cached index hasn't been forged and (b) if a listener is removed
|
||||||
// multiple times, the index may contain a different listener
|
// multiple times, the index may contain a different listener
|
||||||
if ( evls[ index ] === listener )
|
if ( ( index !== undefined ) && ( evls[ index ] === listener ) )
|
||||||
{
|
{
|
||||||
evls[ index ] = undefined;
|
evls[ index ] = undefined;
|
||||||
|
levdata[ ev ] = undefined;
|
||||||
|
|
||||||
this._gaps[ ev ].push( index );
|
this._gaps[ ev ].push( index );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -189,6 +189,31 @@ describe( 'event.Evented', () =>
|
||||||
expect( () => stub[ on ]( ev, "kittens" ) )
|
expect( () => stub[ on ]( ev, "kittens" ) )
|
||||||
.to.throw( TypeError );
|
.to.throw( TypeError );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is highly suspect if the exact same listener is registered
|
||||||
|
* for the same event---it is either a bug, or a questionable
|
||||||
|
* design decision; we assume that the former is far more likely
|
||||||
|
* to be true, and that the latter should be addressed and
|
||||||
|
* resolved.
|
||||||
|
*
|
||||||
|
* Note that this does not prevent a caller from wrapping a
|
||||||
|
* listener in a function that is instead registered as the
|
||||||
|
* listener---this is perfectly valid and has many use cases.
|
||||||
|
*/
|
||||||
|
it( 'does not permit same event listener multiple times', () =>
|
||||||
|
{
|
||||||
|
var ev = 'foo',
|
||||||
|
f = () => {};
|
||||||
|
|
||||||
|
expect( () =>
|
||||||
|
{
|
||||||
|
stub.evDefineEvents( [ ev ] )
|
||||||
|
[ on ]( ev, f )
|
||||||
|
[ on ]( ev, f );
|
||||||
|
} ).to.throw( Error );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue