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

How do I prevent accidental altering of properties of an event in my custom event dispatching system?

本文关键字:事件 属性 意外 系统 何防止 自定义 调度      更新时间:2023-09-26

我构建了一个自定义的事件调度机制。我试图模仿DOM事件实现尽可能多。这仍然是一个草案,但到目前为止运行得相当好。

有一件事让我很困扰,我的事件的监听者很容易改变事件的特定属性,而这些属性对于外部用户来说应该是只读的。我只希望调度事件的实际EventDispatcher能够更改这些属性。

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

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

问题是,我没有明确的想法(至少没有完全重构)如何实现一个合理的健壮的解决方案来解决这个问题。我已经尝试过了(请参阅Eventtarget, currentTargeteventPhase设置的部分,这些设置在我下面提供的代码中被注释掉了),但是这当然失败了(它甚至无法开始)。但是,我希望,通过检查这些部件,您可以了解我的目标,也许您可以提供一个可行的解决方案。它不必是无懈可击的,只要合理地万无一失就可以了。

我试着想象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
你对如何做到这一点有什么建议吗?当然,我不期望有完整的解决方案;只要几个通用的指针就可以了。

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

所以,如果你有其他的解决方案,我还是很想听听。


edit:更新了最终的(-ish)实现。/编辑

重构的(未修饰的)实现,正如它所代表的那样(可能有相当多的比以前少的错误数量):

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;
} )();