如何防止在自定义事件调度系统中意外更改事件属性?

时间:2014-08-04 21:26:25

标签: javascript events encapsulation

我已经构建了一个自定义事件调度机制。我试图尽可能地模仿DOM事件的实现。它仍然是一个选秀,但到目前为止工作得相当好。

令我烦恼的一件事是,对于我的事件的听众而言,当事件实际上对于外人来说实际上是只读的时,它就很容易改变该事件的特定属性。我只希望调度事件的实际EventDispatcher能够改变这些属性。

现在,我意识到基本上任何用户空间Javascript对象都可以被改变,但这并不是我所担心的。我想防止在听众中意外更改Event属性,例如由:

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

问题是,我对如何实现这个问题的合理稳健解决方案没有明确的想法(至少没有完全重构)。我已经尝试过了(请参阅我在下面提供的代码中注释掉的targetcurrentTargeteventPhase Event制定者的部分内容,但是当然悲惨地失败了(开始时甚至都不可行)。但是,我希望通过检查这些部分,您可以看到我的目标,也许您可​​以提供可行的解决方案。它不必是密不透风的,只是相当万无一失。

我试图想象DOM事件如何实现这个技巧(更改event.currentTarget等)并得出结论,它可能不是在(纯)Javascript本身实现,而是在幕后。

如果可能的话,我真的想要阻止克隆事件或类似的实现方法,因为在处理事件阶段和访问不同的侦听器时,DOM事件似乎不会克隆。

这是我目前的实施方式:

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 (只有最相关的部分):

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;

} )();

可能的用法:

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

您对如何使这项工作有任何建议吗?我当然不期待完整的解决方案;只需几个通用指针就可以了。

1 个答案:

答案 0 :(得分:0)

我认为通过将实际的调度例程移动到Event本身,我已经设法提出了一个(可能是临时的)解决方案。我不喜欢这个解决方案,因为我认为Event不应该对实际的调度过程负责,但我现在想不出任何其他的。

所以,如果你有任何解决方案,我仍然希望听到其他解决方案。


使用最终(-ish)实现更新

编辑。的 /修改

重构的(未抛光的)实现,因为它可能(可能有相当多的比早期的bug数量少):

<强> 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 (只有最相关的部分)

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;

} )();