Skip to content
Closed
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
100 changes: 78 additions & 22 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,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}`).
Expand Down Expand Up @@ -174,6 +177,10 @@
if (listening) {
var listeners = obj._listeners || (obj._listeners = {});
listeners[listening.id] = listening;

// Allow the listening to use a counter, instead of tracking
// callbacks for library interop
_listening.interop = false;
}

return obj;
Expand All @@ -186,17 +193,23 @@
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};
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 @@ -212,6 +225,16 @@
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
Expand Down Expand Up @@ -241,30 +264,27 @@
if (!listening) break;

listening.obj.off(name, callback, this);
if (listening.interop) listening.off(name, callback);
}

return this;
};

// The reducing API that removes a callback from the `events` object.
var offApi = function(events, name, callback, options) {
if (!events) return;

var i = 0, 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;
}

var names = name ? [name] : _.keys(events);
names = name ? [name] : _.keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
Expand All @@ -283,11 +303,8 @@
) {
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);
}
}

Expand All @@ -298,7 +315,8 @@
delete events[name];
}
}
return events;

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

// Bind an event to only be triggered a single time. After the first time
Expand All @@ -313,7 +331,7 @@
};

// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
Expand Down Expand Up @@ -348,7 +366,7 @@
};

// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, callback, args) {
var triggerApi = function(objEvents, name, cb, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
Expand All @@ -373,6 +391,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];
};

// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
Expand Down
37 changes: 37 additions & 0 deletions test/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -703,4 +703,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);
});
})();