Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.interop = false;
}

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.interop) 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.interop) 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.interop = true;
this.count = 0;
this._events = void 0;
};

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.interop) {
this._events = eventsApi(offApi, this._events, name, callback, {
context: void 0,
listeners: void 0
});
cleanup = !this._events;
} else {
this.count--;
cleanup = this.count === 0;
}
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.interop) 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
37 changes: 37 additions & 0 deletions test/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,4 +585,41 @@
two.trigger('y', 2);
});

test("#3611 - listenTo is compatible with non-Backbone event libraries", 1, function() {
var obj = _.extend({}, Backbone.Events);
var other = {
events: {},
on: function(name, callback) {
this.events[name] = callback;
},
trigger: function(name) {
this.events[name]();
}
};

obj.listenTo(other, 'test', function() { ok(true); });
other.trigger('test');
});

test("#3611 - stopListening is compatible with non-Backbone event libraries", 1, function() {
var obj = _.extend({}, Backbone.Events);
var other = {
events: {},
on: function(name, callback) {
this.events[name] = callback;
},
off: function() {
this.events = {};
},
trigger: function(name) {
var fn = this.events[name];
if (fn) fn();
}
};

obj.listenTo(other, 'test', function() { ok(false); });
obj.stopListening(other);
other.trigger('test');
equal(_.size(obj._listeningTo), 0);
});
})();