Added Evented#unhookEvent for logic overide

events
Mike Gerwitz 2014-08-08 23:53:07 -04:00
parent 701a49226c
commit 914e4eed78
2 changed files with 143 additions and 5 deletions

View File

@ -259,6 +259,8 @@ module.exports = Trait( 'Evented' )
* (by not forwarding the listener to __supet) and instead invoke * (by not forwarding the listener to __supet) and instead invoke
* __super with a new listener, possibly wrapping the original. * __super with a new listener, possibly wrapping the original.
* *
* See also #unhookEvent.
*
* @O {1} constant time * @O {1} constant time
* *
* @param {string} ev defined event id * @param {string} ev defined event id
@ -328,17 +330,18 @@ module.exports = Trait( 'Evented' )
/** /**
* Remove a previously hooked listener in constant time, preventing it * Remove a previously hooked listener, preventing it from being invoked
* from being invoked on future emits of event EV * on future emits of event EV
* *
* EV must be a valid event identifier. If LISTENER is not registered * EV must be a valid event identifier. If LISTENER is not registered
* for event id EV, no error will occur (since it fulfills the criteria * for event id EV, no error will occur (since it fulfills the criteria
* that it will not be subsequently invoked by event EV). * that it will not be subsequently invoked by event EV).
* *
* Listeners are compared by reference---the exact listener that was * The default algorithm compares listeners by reference---the exact
* registered with EV must be passed for removal. * listener that was registered with EV must be passed for removal;
* note, however, that the algorithm may be overridden by subtypes.
* *
* @O {1} constant time * @O {#unhookEvent} depends on unhooking algorithm
* *
* @param {string} ev defined event id * @param {string} ev defined event id
* @param {Function} listener listener to remove * @param {Function} listener listener to remove
@ -353,6 +356,38 @@ module.exports = Trait( 'Evented' )
throw Error( `Cannot unhook undefined event \`${ev}'` ); throw Error( `Cannot unhook undefined event \`${ev}'` );
} }
this.unhookEvent( ev, listener );
return this;
},
/**
* Remove a previously hooked listener in constant time, preventing it
* from being invoked on future emits of event EV
*
* Listeners are compared by reference---the exact listener that was
* registered with EV must be passed for removal.
*
* Subtypes may override this method to undo behavior introduced by a
* #hookEvent override. Note, however, that the Eventable specification
* prohibits removing arbitrary listeners, and so the removed
* listener(s) must have somehow been related to LISTENER by #hookEvent.
*
* This method will be called even if LISTENER has never been attached
* to event EV---this method performs that check, ensuring that any
* manipulations performed by #hookEvent can be properly handled here.
*
* See also #hookEvent.
*
* @O {1} constant time
*
* @param {string} ev defined event id
* @param {Function} listener listener to remove
*
* @return {Evented} self
*/
'virtual protected unhookEvent'( ev, listener )
{
let evls = this._events[ ev ], let evls = this._events[ ev ],
levdata = listener[ this._evid ] || {}, levdata = listener[ this._evid ] || {},
index = levdata[ ev ]; index = levdata[ ev ];

View File

@ -617,6 +617,109 @@ describe( 'event.Evented', () =>
} ); } );
/**
* Intended to clean up after #hookEvent
*/
describe( '#unhookEvent', function()
{
var ev = 'foo',
f = ()=>{};
it( 'can be overridden by subtypes', ( done ) =>
{
common.checkOverride( EvStub, 'unhookEvent',
( given_ev, given_listener ) =>
{
expect( given_ev ).to.equal( ev );
expect( given_listener ).to.equal( f );
done();
}
)().evDefineEvents( [ ev ] )
.on( ev, f )
.removeListener( ev, f );
} );
/**
* Since #hookEvent can do whatever it pleases with listeners
* (including preventing them from being hooked to begin with, or
* replacing them with another hook), we need to ensure that the
* hook is called regardless of whether the listener is known to be
* attached to a particular event; otherwise, certain types of
* cleanup would not be possible.
*/
it( 'will be called even if listener is not attached', ( done ) =>
{
common.checkOverride( EvStub, 'unhookEvent', ( _, __ ) =>
{
done();
} )().evDefineEvents( [ ev ] )
.removeListener( ev, f );
} );
/**
* This should never be done---since it violates the Eventable
* specification---but it is useful for determining the scope of the
* hook's ability, and is important for proving that, when we modify
* the behavior, we know that the default behavior is suppressed
* (unless we invoke it ourselves).
*/
it( 'can prevent default implementation', () =>
{
let norm = () => called = true,
called = false;
EvStub.extend(
{
'override unhookEvent': ( _, __ ) => {},
} )().evDefineEvents( [ ev ] )
.on( ev, norm )
.removeListener( ev, norm )
.evEmit( ev );
// should have been called since we suppressed unhooking
expect( called ).to.be.true;
} );
/**
* Listeners must ultimately be added/removed by invoking the super
* method (to add listeners to the internal data structures needed
* for dispatch on emit). If we are doing any sort of meaningful
* manipulation, then we must be able to wholly override the
* arguments provided to us.
*
* As an example, if #hookEvent wrapped the given listener $L$ as
* $L'$, which was then hooked for dispatch on some event, then we'd
* need to similarily remove $L'$---not $L$---when invoking
* #removeListener with $L$.
*/
it( 'can override provided event id and listener', () =>
{
let rmf = () => called = true,
called = false,
ev2 = 'snazzlepuss';
EvStub.extend(
{
'override unhookEvent'( _, __ )
{
// use these regardless of what we're given
this.__super( ev, rmf );
},
} )().evDefineEvents( [ ev, ev2 ] )
.on( ev, rmf )
.removeListener( ev2, f )
.evEmit( ev );
// even though we requested to remove F from some arbitrary
// event, our hard-coded removal of RMF should take place
expect( called ).to.be.false;
} );
} );
describe( 'listener metadata', () => describe( 'listener metadata', () =>
{ {
/** /**