From 6aa620a853da9e17a80f9cb6bc6e67c02f6b7d42 Mon Sep 17 00:00:00 2001 From: Richard Davey Date: Tue, 6 Dec 2016 01:38:53 +0000 Subject: [PATCH] Big refactoring to allow for multi-bindings from a single dispatcher. --- v3/src/events/EventBinding.js | 233 +++++++++++++++++++++++++ v3/src/events/EventDispatcher.js | 283 ++++++++----------------------- v3/src/events/EventListener.js | 14 ++ v3/src/events/const.js | 14 ++ 4 files changed, 335 insertions(+), 209 deletions(-) create mode 100644 v3/src/events/EventBinding.js create mode 100644 v3/src/events/EventListener.js create mode 100644 v3/src/events/const.js diff --git a/v3/src/events/EventBinding.js b/v3/src/events/EventBinding.js new file mode 100644 index 000000000..8ffd92498 --- /dev/null +++ b/v3/src/events/EventBinding.js @@ -0,0 +1,233 @@ +var CONST = require('./const'); +var EventListener = require('./EventListener'); + +var EventBinding = function (dispatcher, type) +{ + this.dispatcher = dispatcher; + this.type = type; + this.state = CONST.DISPATCHER_IDLE; + this.active = []; +}; + +EventBinding.prototype.constructor = EventBinding; + +EventBinding.prototype = { + + get: function (callback) + { + for (var i = 0; i < this.active.length; i++) + { + if (this.active[i].callback === callback) + { + return this.active[i]; + } + } + }, + + getIndex: function (callback) + { + for (var i = 0; i < this.active.length; i++) + { + if (this.active[i].callback === callback) + { + return i; + } + } + + return null; + }, + + has: function (callback) + { + return (this.get(callback)); + }, + + add: function (callback, priority, once) + { + var listener = this.get(callback); + + if (!listener) + { + // The listener doesn't exist, so create one + listener = EventListener(this.type, callback, priority, once); + } + else + { + // Listener already exists, abort + return; + } + + if (this.state === CONST.DISPATCHER_IDLE) + { + // The Dispatcher isn't doing anything, so we don't need a pending state + listener.state = CONST.LISTENER_ACTIVE; + } + + this.active.push(listener); + + this.active.sort(this.sortHandler); + }, + + sortHandler: function (listenerA, listenerB) + { + if (listenerB.priority < listenerA.priority) + { + return -1; + } + else if (listenerB.priority > listenerA.priority) + { + return 1; + } + else + { + return 0; + } + }, + + remove: function (callback) + { + if (this.state === CONST.DISPATCHER_IDLE) + { + // The Dispatcher isn't doing anything, so we can remove right away + var i = this.getIndex(callback); + + if (i !== null) + { + this.active.splice(i, 1); + } + } + else if (this.state === CONST.DISPATCHER_DISPATCHING) + { + // The Dispatcher is working, so we flag the listener for removal at the end + var listener = this.get(callback); + + if (listener) + { + listener.state = CONST.LISTENER_REMOVING; + } + } + }, + + dispatch: function (event) + { + if (this.state !== CONST.DISPATCHER_IDLE) + { + throw new Error('Error: Failed to execute \'EventDispatcher.dispatch\' on \'' + this.type + '\': The event is already being dispatched.'); + } + else if (this.active.length === 0) + { + // This was a valid dispatch call, we just had nothing to do ... + return true; + } + + this.state = CONST.DISPATCHER_DISPATCHING; + + console.log('Dispatching', this.active.length, 'listeners'); + + var listener; + + event.reset(this.dispatcher); + + for (var i = 0; i < this.active.length; i++) + { + listener = this.active[i]; + + if (listener.state !== CONST.LISTENER_ACTIVE) + { + continue; + } + + listener.callback.call(this.dispatcher, event); + + // Has the callback changed the state of this binding? + if (this.state !== CONST.DISPATCHER_DISPATCHING) + { + // Yup! Let's break out + break; + } + + // Was it a 'once' listener? + if (listener.once) + { + listener.state = CONST.LISTENER_REMOVING; + } + + // Has the event been halted by the callback? + if (!event._propagate) + { + // Break out, a listener has called Event.stopPropagation + break; + } + } + + // Dispatch over, or aborted + if (this.state === CONST.DISPATCHER_REMOVING) + { + this.removeAll(); + } + else if (this.state === CONST.DISPATCHER_DESTROYED) + { + this.dispatcher.delete(this.type); + } + else + { + // All done, just purge the list + this.tidy(); + + this.state = CONST.DISPATCHER_IDLE; + } + }, + + // Removes all listeners + // If this is currently being dispatched then don't remove 'pending' listeners + // (i.e. ones that were added during the dispatch), only active ones + removeAll: function () + { + if (this.state === CONST.DISPATCHER_IDLE) + { + this.active.length = 0; + } + else + { + var i = this.active.length; + + while (i--) + { + if (this.active[i].state !== CONST.LISTENER_PENDING) + { + this.active.slice(i, 1); + } + } + + this.state = CONST.DISPATCHER_IDLE; + } + }, + + tidy: function () + { + var i = this.active.length; + + while (i--) + { + if (this.active[i].state === CONST.LISTENER_REMOVING) + { + this.active.slice(i, 1); + } + else if (this.active[i].state === CONST.LISTENER_PENDING) + { + this.active[i].state === CONST.LISTENER_ACTIVE; + } + } + }, + + destroy: function () + { + this.active.length = 0; + this.dispatcher = undefined; + this.type = ''; + this.state = CONST.DISPATCHER_DESTROYED; + } + +}; + +module.exports = EventBinding; diff --git a/v3/src/events/EventDispatcher.js b/v3/src/events/EventDispatcher.js index f11556b94..b16f1e06f 100644 --- a/v3/src/events/EventDispatcher.js +++ b/v3/src/events/EventDispatcher.js @@ -1,88 +1,42 @@ +var EventBinding = require('./EventBinding'); + var EventDispatcher = function () { - this.listeners = {}; - - this._state = EventDispatcher.STATE_PENDING; + this.bindings = {}; }; -EventDispatcher.STATE_PENDING = 0; -EventDispatcher.STATE_DISPATCHING = 1; -EventDispatcher.STATE_REMOVING_ALL = 2; -EventDispatcher.STATE_DESTROYED = 3; - EventDispatcher.prototype.constructor = EventDispatcher; EventDispatcher.prototype = { - // Private - add: function (type, listener, priority, isOnce) + getBinding: function (type) { - if (this.listeners === undefined) + if (this.bindings.hasOwnProperty(type)) { - // Has the EventDispatcher been destroyed? - return; - } - - // console.log('Add listener', type, listener); - - if (!this.listeners[type]) - { - this.listeners[type] = []; - } - - var listeners = this.listeners[type]; - - if (this.has(type, listener)) - { - this.update(type, listener, priority, isOnce); - } - else - { - listeners.push({ listener: listener, priority: priority, isOnce: isOnce, toRemove: false }); - } - - listeners.sort(this.sortHandler); - }, - - sortHandler: function (listenerA, listenerB) - { - if (listenerB.priority < listenerA.priority) - { - return -1; - } - else if (listenerB.priority > listenerA.priority) - { - return 1; - } - else - { - return 0; + return this.bindings[type]; } }, - update: function (type, listener, priority, isOnce) + createBinding: function (type) { - var listeners = this.listeners[type]; - - for (var i = 0; i < listeners.length; i++) + if (!this.getBinding(type)) { - if (listeners[i].listener === listener) - { - // They're trying to add the same listener again, so just update the priority + once - listeners[i].priority = priority; - listeners[i].isOnce = isOnce; - } + this.bindings[type] = new EventBinding(this, type); } + return this.bindings[type]; }, - // Need to test what happens if array is sorted during DISPATCH phase (does it screw it all up?) - on: function (type, listener, priority) { if (priority === undefined) { priority = 0; } - this.add(type, listener, priority, false); + var binding = this.createBinding(type); + + if (binding) + { + binding.add(type, listener, priority, false); + } return this; }, @@ -91,69 +45,39 @@ EventDispatcher.prototype = { { if (priority === undefined) { priority = 0; } - this.add(type, listener, priority, true); + var binding = this.createBinding(type); + + if (binding) + { + binding.add(type, listener, priority, true); + } return this; }, - total: function (type) - { - if (!this.listeners || !this.listeners[type]) - { - return -1; - } - - return this.listeners[type].length; - }, - has: function (type, listener) { - if (!this.listeners || !this.listeners[type]) + var binding = this.getBinding(type); + + if (binding) + { + return binding.has(listener); + } + else { return false; } - - var listeners = this.listeners[type]; - - for (var i = 0; i < listeners.length; i++) - { - if (listeners[i].listener === listener) - { - return true; - } - } - - return false; }, // Removes an event listener. // If there is no matching listener registered with the EventDispatcher, a call to this method has no effect. off: function (type, listener) { - if (!this.listeners || !this.listeners[type]) + var binding = this.getBinding(type); + + if (binding) { - return this; - } - - var listeners = this.listeners[type]; - - for (var i = 0; i < listeners.length; i++) - { - if (listeners[i].listener === listener) - { - if (this._state === EventDispatcher.STATE_DISPATCHING) - { - console.log('Flag listener for removal', type); - listeners[i].toRemove = true; - } - else - { - console.log('Remove listener', type); - listeners.splice(i, 1); - } - - break; - } + binding.remove(listener); } return this; @@ -161,130 +85,71 @@ EventDispatcher.prototype = { dispatch: function (event) { - // Add in a dispatch lock, to stop the dispatcher from being invoked during an event callback + var binding; - if (this._state !== EventDispatcher.STATE_PENDING || !this.listeners[event.type]) + if (Array.isArray(event)) { - return false; - } - - var listeners = this.listeners[event.type]; - - // This was a valid dispatch call, we just had nothing to do ... - if (listeners.length === 0) - { - return true; - } - - this._state = EventDispatcher.STATE_DISPATCHING; - - event.reset(this); - - var toRemove = []; - - var entry; - var entries = listeners.slice(); - // var entries = listeners; - - console.log('Dispatching', entries.length, 'listeners'); - - for (var i = 0; i < entries.length; i++) - { - entry = entries[i]; - - if (entry.toRemove) + for (var i = 0; i < event.length; i++) { - toRemove.push(entry); - continue; + binding = this.getBinding(event[i].type); + + if (binding) + { + return binding.dispatch(event[i]); + } } - - // Add Custom Events - // If this adjusts the entries.length for any reason, the reference is still valid - entry.listener.call(this, event); - - // Has the callback done something disastrous? Like called removeAll, or nuked the dispatcher? - if (this._state !== EventDispatcher.STATE_DISPATCHING) - { - // Yup! Let's get out of here ... - break; - } - - // Was a 'once' or was removed during the callback - if (entry.isOnce || entry.toRemove) - { - toRemove.push(entry); - } - - // Has the event been halted? - if (!event._propagate) - { - // Break out, a listener has called Event.stopPropagation - break; - } - } - - if (this._state === EventDispatcher.STATE_REMOVING_ALL) - { - this.removeAll(); - } - else if (this._state === EventDispatcher.STATE_DESTROYED) - { - this.destroy(); } else { - // Anything in the toRemove list? + binding = this.getBinding(event.type); - console.log('Cleaning out', toRemove.length, 'listeners'); - - for (i = 0; i < toRemove.length; i++) + if (binding) { - this.off(event.type, toRemove[i].listener); + return binding.dispatch(event); } - - toRemove.length = 0; - - this._state = EventDispatcher.STATE_PENDING; } - - return true; }, // Removes all listeners, but retains the event type entries - removeAll: function () + removeAll: function (type) { - if (this._state === EventDispatcher.STATE_DISPATCHING) - { - this._state = EventDispatcher.STATE_REMOVING_ALL; + var binding = this.getBinding(type); - return; - } - - for (var eventType in this.listeners) + if (binding) { - this.listeners[eventType].length = 0; + binding.removeAll(); } return this; }, + delete: function (type) + { + var binding = this.getBinding(type); + + if (binding) + { + binding.destroy(); + + delete this.bindings[type]; + } + + return this; + }, + + deleteAll: function () + { + for (var binding in this.bindings) + { + binding.destroy(); + } + + this.bindings = {}; + }, + destroy: function () { - if (this._state === EventDispatcher.STATE_DISPATCHING) - { - this._state = EventDispatcher.STATE_DESTROYED; - - return; - } - - for (var eventType in this.listeners) - { - this.listeners[eventType].length = 0; - - delete this.listeners[eventType]; - } - - this.listeners = undefined; + // What would it do any differently to deleteAll? } }; diff --git a/v3/src/events/EventListener.js b/v3/src/events/EventListener.js new file mode 100644 index 000000000..afb4479eb --- /dev/null +++ b/v3/src/events/EventListener.js @@ -0,0 +1,14 @@ +var CONST = require('./const'); + +var EventListener = function (type, callback, priority, once) +{ + return { + type: type, + callback: callback, + priority: priority, + once: once, + state: CONST.LISTENER_PENDING + }; +}; + +module.exports = EventListener; diff --git a/v3/src/events/const.js b/v3/src/events/const.js new file mode 100644 index 000000000..eb7b27db7 --- /dev/null +++ b/v3/src/events/const.js @@ -0,0 +1,14 @@ +var EVENT_CONST = { + + DISPATCHER_IDLE: 0, + DISPATCHER_DISPATCHING: 1, + DISPATCHER_REMOVING: 2, + DISPATCHER_DESTROYED: 3, + + LISTENER_PENDING: 4, + LISTENER_ACTIVE: 5, + LISTENER_REMOVING: 6 + +}; + +module.exports = EVENT_CONST;