Big refactoring to allow for multi-bindings from a single dispatcher.

This commit is contained in:
Richard Davey 2016-12-06 01:38:53 +00:00
parent f3f6c98896
commit 6aa620a853
4 changed files with 335 additions and 209 deletions

View file

@ -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;

View file

@ -1,88 +1,42 @@
var EventBinding = require('./EventBinding');
var EventDispatcher = function () var EventDispatcher = function ()
{ {
this.listeners = {}; this.bindings = {};
this._state = EventDispatcher.STATE_PENDING;
}; };
EventDispatcher.STATE_PENDING = 0;
EventDispatcher.STATE_DISPATCHING = 1;
EventDispatcher.STATE_REMOVING_ALL = 2;
EventDispatcher.STATE_DESTROYED = 3;
EventDispatcher.prototype.constructor = EventDispatcher; EventDispatcher.prototype.constructor = EventDispatcher;
EventDispatcher.prototype = { EventDispatcher.prototype = {
// Private getBinding: function (type)
add: function (type, listener, priority, isOnce)
{ {
if (this.listeners === undefined) if (this.bindings.hasOwnProperty(type))
{ {
// Has the EventDispatcher been destroyed? return this.bindings[type];
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;
} }
}, },
update: function (type, listener, priority, isOnce) createBinding: function (type)
{ {
var listeners = this.listeners[type]; if (!this.getBinding(type))
for (var i = 0; i < listeners.length; i++)
{ {
if (listeners[i].listener === listener) this.bindings[type] = new EventBinding(this, type);
{
// They're trying to add the same listener again, so just update the priority + once
listeners[i].priority = priority;
listeners[i].isOnce = isOnce;
}
} }
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) on: function (type, listener, priority)
{ {
if (priority === undefined) { priority = 0; } 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; return this;
}, },
@ -91,69 +45,39 @@ EventDispatcher.prototype = {
{ {
if (priority === undefined) { priority = 0; } 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; return this;
}, },
total: function (type)
{
if (!this.listeners || !this.listeners[type])
{
return -1;
}
return this.listeners[type].length;
},
has: function (type, listener) has: function (type, listener)
{ {
if (!this.listeners || !this.listeners[type]) var binding = this.getBinding(type);
if (binding)
{
return binding.has(listener);
}
else
{ {
return false; 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. // Removes an event listener.
// If there is no matching listener registered with the EventDispatcher, a call to this method has no effect. // If there is no matching listener registered with the EventDispatcher, a call to this method has no effect.
off: function (type, listener) off: function (type, listener)
{ {
if (!this.listeners || !this.listeners[type]) var binding = this.getBinding(type);
if (binding)
{ {
return this; binding.remove(listener);
}
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;
}
} }
return this; return this;
@ -161,130 +85,71 @@ EventDispatcher.prototype = {
dispatch: function (event) 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; for (var i = 0; i < event.length; i++)
}
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)
{ {
toRemove.push(entry); binding = this.getBinding(event[i].type);
continue;
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 else
{ {
// Anything in the toRemove list? binding = this.getBinding(event.type);
console.log('Cleaning out', toRemove.length, 'listeners'); if (binding)
for (i = 0; i < toRemove.length; i++)
{ {
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 // Removes all listeners, but retains the event type entries
removeAll: function () removeAll: function (type)
{ {
if (this._state === EventDispatcher.STATE_DISPATCHING) var binding = this.getBinding(type);
{
this._state = EventDispatcher.STATE_REMOVING_ALL;
return; if (binding)
}
for (var eventType in this.listeners)
{ {
this.listeners[eventType].length = 0; binding.removeAll();
} }
return this; 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 () destroy: function ()
{ {
if (this._state === EventDispatcher.STATE_DISPATCHING) // What would it do any differently to deleteAll?
{
this._state = EventDispatcher.STATE_DESTROYED;
return;
}
for (var eventType in this.listeners)
{
this.listeners[eventType].length = 0;
delete this.listeners[eventType];
}
this.listeners = undefined;
} }
}; };

View file

@ -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;

14
v3/src/events/const.js Normal file
View file

@ -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;