How do I prevent accidentally changing event properties in my custom event dispatcher system?

I have created a custom event dispatching mechanism. I try to mimic the DOM event implementation as much as possible. It's still a rough draft, but it still works well.

One thing that bothers me is that it is quite easy for my event listeners to change the specific properties of that event when they really should be read-only to outsiders. I only need the actual EventDispatcher

one that dispatches the event to be able to change these properties.

Now I understand that basically any custom Javascript object can be modified, but that's not what I'm worried about. I want to prevent accidentally changing properties Event

in listeners, eg. by:

function someListener( event ) {
    if( event.currentTarget = this ) { // whoops, we've accidentally overwritten event.currentTarget
       // do something
    }
}

      

The problem is, I don't have a clear idea (at least not without a complete reorganization) on how to implement a reasonably robust solution to this problem. I tried it (see the parts of the settings target

, currentTarget

and eventPhase

Event

that are commented out in the code I provide below), but it certainly failed (it wasn't even viable to start with). I hope, however, that by looking at these parts, you can see what I am aiming for, and that perhaps you can come up with an acceptable solution. It doesn't have to be airtight, just reasonable, reliable.

I tried to imagine how DOM events would implement this trickery (change event.currentTarget

, etc.) and came to the conclusion that it is probably not implemented in (pure) Javascript itself, but under the hood.

I'd really like to prevent cloning events or similar implementation ideas if possible, since DOM events don't seem to be cloned when handling event phases and visiting different listeners.

Here's my current implementation:

codifier.event.Event

codifier.event.Event = ( function() {

    function Event( type, bubbles, cancelable ) {

        if( !( this instanceof Event ) ) {
            return new Event( type, bubbles, cancelable );
        }

        let privateVars = {
            type: type,
            target: null,
            currentTarget: null,
            eventPhase: Event.NONE,
            bubbles: !!bubbles,
            cancelable: !!cancelable,
            defaultPrevented: false,
            propagationStopped: false,
            immediatePropagationStopped: false
        }

        this.preventDefault = function() {
            if( privateVars.cancelable ) {
                privateVars.defaultPrevented = true;
            }
        }

        this.stopPropagation = function() {
            privateVars.propagationStopped = true;
        }

        this.stopImmediatePropagation = function() {
            privateVars.immediatePropagationStopped = true;
            this.stopPropagation();
        }

        Object.defineProperties( this, {
            'type': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.type;
                }
            },
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) || null !== privateVars.target ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.target = value;
                }
            },
            'currentTarget': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.currentTarget;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.currentTarget = value;
                }
            },
            'eventPhase': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.eventPhase;
                },
                set: function( value ) {
                    /* this was a rather silly attempt
                    if( !( this instanceof codifier.event.EventDispatcher ) ) {
                        throw new TypeError( 'setting a property that has only a getter' );
                    }
                    */
                    privateVars.eventPhase = value;
                }
            },
            'bubbles': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.bubbles;
                }
            },
            'cancelable': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.cancelable;
                }
            },
            'defaultPrevented': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.defaultPrevented;
                }
            },
            'propagationStopped': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.propagationStopped;
                }
            },
            'immediatePropagationStopped': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.immediatePropagationStopped;
                }
            }
        } );

        Object.freeze( this );
    }

    Event.NONE            = 0;
    Event.CAPTURING_PHASE = 1;
    Event.AT_TARGET       = 2;
    Event.BUBBLING_PHASE  = 3;

    Object.freeze( Event );
    Object.freeze( Event.prototype );

    return Event;

} )();

      

codifier.event.EventDispatcher (only the most relevant parts):

codifier.event.EventDispatcher = ( function() {

    function EventDispatcher( target, ancestors ) {

        if( !( this instanceof EventDispatcher ) ) {
            return new EventDispatcher( target, ancestors );
        }

        let privateVars = {
            target: target === Object( target ) ? target : this,
            ancestors: [],
            eventListeners: {}
        }

        this.clearAncestors = function() {
            privateVars.ancestors = [];
        }

        this.setAncestors = function( ancestors ) {
            this.clearAncestors();
            if( Array.isArray( ancestors ) ) {
                ancestors.forEach( function( ancestor ) {
                    if( ancestor instanceof EventDispatcher ) {
                        privateVars.ancestors.push( ancestor );
                    }
                } );
            }
        }

        this.dispatchEvent = function( event ) {
            if( event instanceof codifier.event.Event ) {
                if( event.eventPhase === Event.NONE && null === event.target ) {
                    event.target        = privateVars.target;
                    event.currentTarget = privateVars.target;

                    let ancestors = privateVars.ancestors;

                    // Event.CAPTURING_PHASE
                    event.eventPhase = Event.CAPTURING_PHASE;
                    for( let c = ancestors.length - 1; !event.propagationStopped && c >= 0; c-- ) {
                        let ancestor = ancestors[ c ];
                        ancestor.dispatchEvent( event );
                    }

                    // Event.AT_TARGET
                    event.eventPhase = Event.AT_TARGET;
                    if( !event.propagationStopped && this.hasEventListenersForEvent( event.type, true ) ) {
                        for( let listener of privateVars.eventListeners[ event.type ][ Event.CAPTURING_PHASE ].values() ) {
                            if( event.immediatePropagationStopped ) {
                                break;
                            }
                            listener.call( privateVars.target, event );
                        }
                    }

                    if( !event.propagationStopped && this.hasEventListenersForEvent( event.type, false ) ) {
                        for( let listener of privateVars.eventListeners[ event.type ][ Event.BUBBLING_PHASE ].values() ) {
                            if( event.immediatePropagationStopped ) {
                                break;
                            }
                            listener.call( privateVars.target, event );
                        }
                    }

                    // Event.BUBBLING_PHASE
                    if( event.bubbles ) {
                        event.eventPhase = Event.BUBBLING_PHASE;
                        for( let b = 0, l = ancestors.length; !event.propagationStopped && b < l; b++ ) {
                            let ancestor = ancestors[ b ];
                            ancestor.dispatchEvent( event );
                        }
                    }

                    event.eventPhase    = Event.NONE;
                    event.currentTarget = null;
                }
                else if( event.eventPhase == Event.CAPTURING_PHASE || event.eventPhase == Event.BUBBLING_PHASE ) {
                    event.currentTarget = privateVars.target;

                    if( !event.propagationStopped && this.hasEventListenersForEvent( event.type, event.eventPhase == Event.CAPTURING_PHASE ) ) {
                        for( let listener of privateVars.eventListeners[ event.type ][ event.eventPhase ].values() ) {
                            if( event.immediatePropagationStopped ) {
                                break;
                            }
                            listener.call( privateVars.target, event );
                        }
                    }
                }
            }
        }

        Object.freeze( this );

        this.setAncestors( ancestors );
    }

    Object.freeze( EventDispatcher );
    Object.freeze( EventDispatcher.prototype );

    return EventDispatcher;

} )();

      

Possible use:

let SomeEventEmittingObject = ( function() {

    function SomeEventEmittingObject() {

        let privateVars = {
            eventDispatcher: new EventDispatcher( this ),
            value: 0
        }

        // this.addEventListener ... proxy to eventDispatcher.addEventListener
        // this.removeEventListener ... proxy to eventDispatcher.removeEventListener
        // etc.

        Object.defineProperty( this, 'value', {
            set: function( value ) {
                privateVars.value = value;
                privateVars.eventDispatcher.dispatchEvent( new Event( 'change', true, false ) );
            },
            get: function()  {
                return privateVars.value;
            }
        } );
    }

    return SomeEventEmittingObject;

} )();

let obj = new SomeEventEmittingObject();
obj.value = 5; // dispatches 'change' event

      

Do you have any suggestions on how to make this work? Of course, I don't expect complete solutions; just a few general pointers would be great.

+3


source to share


1 answer


I think I was able to find a (possibly temporary) solution by moving the actual dispatch procedure to Event

. I don't like this solution as I don't think I Event

should be responsible for the actual dispatching process, but at the moment I couldn't think of anything else.

So, I will still love alternative solutions if you have one.


edit: updated with final (-ish) implementation. / change

Implemented implementation (unpolished) as it is (may have quite a lot less errors than before):



codifier.event.Event

codifier.event.Event = ( function() {

    function Event( type, bubbles, cancelable, detail ) {

        if( !( this instanceof Event ) ) {
            return new Event( type, bubbles, cancelable, detail );
        }

        let privateVars = {
            instance: this,
            dispatched: false,
            type: type,
            target: null,
            currentTarget: null,
            eventPhase: Event.NONE,
            bubbles: !!bubbles,
            cancelable: !!cancelable,
            detail: undefined == detail ? null : detail,
            defaultPrevented: false,
            propagationStopped: false,
            immediatePropagationStopped: false
        }

        let processListeners = function( listeners ) {
            for( let listener of listeners ) {
                if( privateVars.immediatePropagationStopped ) {
                    return false;
                }
                listener.call( privateVars.currentTarget, privateVars.instance );
            }
            return true;
        }

        let processDispatcher = function( dispatcher, useCapture ) {
            privateVars.currentTarget = dispatcher.target;
            return processListeners( dispatcher.getEventListenersForEvent( privateVars.type, useCapture ) );
        }

        let processDispatchers = function( dispatchers, useCapture ) {
            for( let i = 0, l = dispatchers.length; !privateVars.propagationStopped && i < l; i++ ) {
                let dispatcher = dispatchers[ i ];
                if( !processDispatcher( dispatcher, useCapture ) ) {
                    return false;
                }
            }

            return true;
        }

        this.dispatch = function( dispatcher ) {
            if( privateVars.dispatched ) {
                throw new Error( 'This event has already been dispatched.' );
                return false;
            }

            if( !( dispatcher instanceof codifier.event.EventDispatcher ) ) {
                throw new Error( 'Only EventDispatchers are allowed to dispatch an event.' );
                return false;
            }

            privateVars.dispatched = true;
            let ancestors = dispatcher.getAncestors();
            do_while_label: // javascript needs a label to reference to break out of outer loops
            do {
                switch( privateVars.eventPhase ) {
                    case Event.NONE:
                        privateVars.target = dispatcher.target;
                        privateVars.currentTarget = dispatcher.target;
                        privateVars.eventPhase = Event.CAPTURING_PHASE;
                    break;
                    case Event.CAPTURING_PHASE:
                        if( !processDispatchers( ancestors.slice().reverse(), true ) ) {
                            break do_while_label;
                        }
                        privateVars.eventPhase = Event.AT_TARGET;
                    break;
                    case Event.AT_TARGET:
                        privateVars.currentTarget = dispatcher.target;
                        if( !processDispatcher( dispatcher, true ) || !processDispatcher( dispatcher, false ) ) {
                            break do_while_label;
                        }
                        privateVars.eventPhase = privateVars.bubbles ? Event.BUBBLING_PHASE : Event.NONE;
                    break;
                    case Event.BUBBLING_PHASE:
                        if( !processDispatchers( ancestors, false ) ) {
                            break do_while_label;
                        }
                        privateVars.currentTarget = null;
                        privateVars.eventPhase = Event.NONE;
                    break;
                    default:
                        // we should never be able to reach this
                        throw new Error( 'This event encountered an inconsistent internal state' );
                    break do_while_label; // break out of the do...while loop.
                }

            } while( !privateVars.propagationStopped && privateVars.eventPhase !== Event.NONE );

            privateVars.currentTarget = null;
            privateVars.eventPhase = Event.NONE;

            return !privateVars.defaultPrevented;
        }

        this.preventDefault = function() {
            if( privateVars.cancelable ) {
                privateVars.defaultPrevented = true;
            }
        }

        this.stopPropagation = function() {
            privateVars.propagationStopped = true;
        }

        this.stopImmediatePropagation = function() {
            privateVars.immediatePropagationStopped = true;
            this.stopPropagation();
        }

        Object.defineProperties( this, {
            'type': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.type;
                }
            },
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                }
            },
            'currentTarget': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.currentTarget;
                }
            },
            'eventPhase': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.eventPhase;
                }
            },
            'bubbles': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.bubbles;
                }
            },
            'cancelable': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.cancelable;
                }
            },
            'detail': {
                configurable: false,
                enumerable: true,
                get: function() {
                    return privateVars.detail;
                }
            },
            'defaultPrevented': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.defaultPrevented;
                }
            }
        } );

        Object.freeze( this );
    }

    Event.NONE            = 0;
    Event.CAPTURING_PHASE = 1;
    Event.AT_TARGET       = 2;
    Event.BUBBLING_PHASE  = 3;

    Object.freeze( Event );
    Object.freeze( Event.prototype );

    return Event;

} )();

      

codifier.event.EventDispatcher (only the most relevant parts) :

codifier.event.EventDispatcher = ( function() {

    function EventDispatcher( target, ancestors ) {

        if( !( this instanceof EventDispatcher ) ) {
            return new EventDispatcher( target, ancestors );
        }

        let privateVars = {
            instance: this,
            target: target === Object( target ) ? target : this,
            ancestors: [],
            eventListeners: {}
        }

        this.clearAncestors = function() {
            privateVars.ancestors = [];
        }

        this.setAncestors = function( ancestors ) {
            this.clearAncestors();
            if( Array.isArray( ancestors ) ) {
                ancestors.forEach( function( ancestor ) {
                    if( ancestor instanceof EventDispatcher ) {
                        privateVars.ancestors.push( ancestor );
                    }
                } );
            }
        }

        this.getAncestors = function() {
            return privateVars.ancestors;
        }

        this.getEventListenersForEvent = function( type, useCapture ) {
            if( !this.hasEventListenersForEvent( type, useCapture ) ) {
                return [];
            }

            let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
            return privateVars.eventListeners[ type ][ eventPhase ].values();
        }

        this.hasEventListenersForEvent = function( type, useCapture ) {
            if( !privateVars.eventListeners.hasOwnProperty( type ) ) {
                return false;
            }

            let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
            if( !privateVars.eventListeners[ type ].hasOwnProperty( eventPhase ) ) {
                return false;
            }

            return privateVars.eventListeners[ type ][ eventPhase ].size > 0;
        }

        this.hasEventListener = function( type, listener, useCapture ) {
            if( !this.hasEventListenersForEvent( type, useCapture ) ) {
                return false;
            }

            let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
            return privateVars.eventListeners[ type ][ eventPhase ].has( listener );
        }

        this.addEventListener = function( type, listener, useCapture ) {
            if( !this.hasEventListener( type, listener, useCapture ) ) {
                if( !privateVars.eventListeners.hasOwnProperty( type ) ) {
                    privateVars.eventListeners[ type ] = {};
                }

                let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
                if( !privateVars.eventListeners[ type ].hasOwnProperty( eventPhase ) ) {
                    privateVars.eventListeners[ type ][ eventPhase ] = new Map();
                }
                privateVars.eventListeners[ type ][ eventPhase ].set( listener, listener );
            }
        }

        this.removeEventListener = function( type, listener, useCapture ) {
            if( this.hasEventListener( type, listener, useCapture ) ) {
                let eventPhase = useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE;
                privateVars.eventListeners[ type ][ eventPhase ].delete( listener );
            }
        }

        this.dispatchEvent = function( event ) {
            if( event instanceof codifier.event.Event ) {
                return event.dispatch( privateVars.instance );
            }

            return false;
        }

        this.clear = function() {
            Object.getOwnPropertyNames( privateVars.eventListeners ).forEach( function( type ) {
                Object.getOwnPropertyNames( privateVars.eventListeners[ type ] ).forEach( function( eventPhase ) {
                    privateVars.eventListeners[ type ][ eventPhase ].clear();
                    delete privateVars.eventListeners[ type ][ eventPhase ];
                } );
                delete privateVars.eventListeners[ type ];
            } );
        }

        Object.defineProperties( this, {
            'target': {
                configurable: false,
                enumerable: false,
                get: function() {
                    return privateVars.target;
                }
            }
        } );

        Object.freeze( this );

        this.setAncestors( ancestors );
    }

    Object.freeze( EventDispatcher );
    Object.freeze( EventDispatcher.prototype );

    return EventDispatcher;

} )();

      

0


source







All Articles