Skip to content

Commit

Permalink
Use the public on method when listening
Browse files Browse the repository at this point in the history
This uses a private `listening` var to share state between a Backbone
"listener" and "listenee", instead of using a private `internalOn()` to
share state. This allows `#listenTo` to use the public `#on` method and
keeps interop between Backbone and any other event library.
  • Loading branch information
jridgewell committed May 20, 2015
1 parent 75c2f12 commit b076de8
Showing 1 changed file with 89 additions and 41 deletions.
130 changes: 89 additions & 41 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
// Regular expression used to split event strings.
var eventSplitter = /\s+/;

// A private global variable to share between listeners and listenees.
var _listening;

// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`), reducing them by manipulating `memo`.
Expand All @@ -96,7 +99,7 @@
var i = 0, names;
if (name && typeof name === 'object') {
// Handle event maps.
for (names = _.keys(name); i < names.length ; i++) {
for (names = _.keys(name); i < names.length; i++) {
memo = iteratee(memo, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
Expand All @@ -113,43 +116,46 @@
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
Events.on = function(name, callback, context) {
return internalOn(this, name, callback, context);
};

// An internal use `on` function, used to guard the `listening` argument from
// the public API.
var internalOn = function(obj, name, callback, context, listening) {
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
context: context,
ctx: obj,
listening: listening
this._events = eventsApi(onApi, this._events || {}, name, callback, {
context: context,
ctx: this,
listening: _listening
});

if (listening) {
var listeners = obj._listeners || (obj._listeners = {});
listeners[listening.id] = listening;
if (_listening) {
var listeners = this._listeners || (this._listeners = {});
listeners[_listening.id] = _listening;
// Allow the listening to use a counter, instead of tracking
// callbacks for library interop
_listening.backbone = true;
}

return obj;
return this;
};

// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to.
Events.listenTo = function(obj, name, callback) {
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
var listening = _listening = listeningTo[id];

// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
this._listenId || (this._listenId = _.uniqueId('l'));
listening = _listening = listeningTo[id] = new Listening(this, obj);
}

// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
// Bind callbacks on obj.
var error = tryCatchOn(obj, name, callback, this);
_listening = void 0;

if (error) throw error;
// If the target obj is not Backbone.Events, track events manually.
if (!listening.backbone) listening.on(name, callback);

return this;
};

Expand All @@ -165,16 +171,27 @@
return events;
};

// An try-catch guarded #on function, to prevent poisoning the global
// `_listening` variable.
var tryCatchOn = function(obj, name, callback, context) {
try {
obj.on(name, callback, context);
} catch (e) {
return e;
}
};

// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
Events.off = function(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
context: context,
listeners: this._listeners
context: context,
listeners: this._listeners
});

return this;
};

Expand All @@ -185,7 +202,6 @@
if (!listeningTo) return this;

var ids = obj ? [obj._listenId] : _.keys(listeningTo);

for (var i = 0; i < ids.length; i++) {
var listening = listeningTo[ids[i]];

Expand All @@ -194,9 +210,8 @@
if (!listening) break;

listening.obj.off(name, callback, this);
if (!listening.backbone) listening.off(name, callback);
}
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;

return this;
};

Expand All @@ -205,16 +220,13 @@
// No events to consider.
if (!events) return;

var i = 0, length, listening;
var context = options.context, listeners = options.listeners;
var i = 0, names;

// Delete all events listeners and "drop" events.
if (!name && !callback && !context) {
var ids = _.keys(listeners);
for (; i < ids.length; i++) {
listening = listeners[ids[i]];
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
// Delete all event listeners and "drop" events.
if (!name && !context && !callback) {
for (names = _.keys(listeners); i < names.length; i++) {
listeners[names[i]].cleanup();
}
return;
}
Expand All @@ -227,7 +239,7 @@
// Bail out if there are no events stored.
if (!handlers) break;

// Replace events if there are any remaining. Otherwise, clean up.
// Find any remaining events.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
Expand All @@ -238,21 +250,19 @@
) {
remaining.push(handler);
} else {
listening = handler.listening;
if (listening && --listening.count === 0) {
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
var listening = handler.listening;
if (listening) listening.off(name, callback);
}
}

// Update tail event if the list has any events. Otherwise, clean up.
// Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}

if (_.size(events)) return events;
};

Expand Down Expand Up @@ -327,6 +337,44 @@
}
};

// A listening class that tracks and cleans up memory bindings
// when all callbacks have been offed.
var Listening = function(listener, obj) {
this.id = listener._listenId;
this.listener = listener;
this.obj = obj;
this.backbone = false;
this.count = 0;
this._events = {};
};

Listening.prototype.on = Events.on;

// Offs a callback (or several).
// Uses an optimized counter if the listenee uses Backbone.Events.
// Otherwise, falls back to manual tracking to support events
// library interop.
Listening.prototype.off = function(name, callback) {
var cleanup;
if (this.backbone) {
this.count--;
cleanup = this.count === 0;
} else {
this._events = eventsApi(offApi, this._events, name, callback, {
context: void 0,
listeners: void 0
});
cleanup = !this._events;
}
if (cleanup) this.cleanup();
};

// Cleans up memory bindings between the listener and the listenee.
Listening.prototype.cleanup = function() {
delete this.listener._listeningTo[this.obj._listenId];
if (this.backbone) delete this.obj._listeners[this.id];
};

// Proxy Underscore methods to a Backbone class' prototype using a
// particular attribute as the data argument
var addMethod = function(length, method, attribute) {
Expand Down

0 comments on commit b076de8

Please sign in to comment.