Skip to content

Commit b076de8

Browse files
committed
Use the public on method when listening
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.
1 parent 75c2f12 commit b076de8

File tree

1 file changed

+89
-41
lines changed

1 file changed

+89
-41
lines changed

backbone.js

Lines changed: 89 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@
8787
// Regular expression used to split event strings.
8888
var eventSplitter = /\s+/;
8989

90+
// A private global variable to share between listeners and listenees.
91+
var _listening;
92+
9093
// Iterates over the standard `event, callback` (as well as the fancy multiple
9194
// space-separated events `"change blur", callback` and jQuery-style event
9295
// maps `{event: callback}`), reducing them by manipulating `memo`.
@@ -96,7 +99,7 @@
9699
var i = 0, names;
97100
if (name && typeof name === 'object') {
98101
// Handle event maps.
99-
for (names = _.keys(name); i < names.length ; i++) {
102+
for (names = _.keys(name); i < names.length; i++) {
100103
memo = iteratee(memo, names[i], name[names[i]], opts);
101104
}
102105
} else if (name && eventSplitter.test(name)) {
@@ -113,43 +116,46 @@
113116
// Bind an event to a `callback` function. Passing `"all"` will bind
114117
// the callback to all events fired.
115118
Events.on = function(name, callback, context) {
116-
return internalOn(this, name, callback, context);
117-
};
118-
119-
// An internal use `on` function, used to guard the `listening` argument from
120-
// the public API.
121-
var internalOn = function(obj, name, callback, context, listening) {
122-
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
123-
context: context,
124-
ctx: obj,
125-
listening: listening
119+
this._events = eventsApi(onApi, this._events || {}, name, callback, {
120+
context: context,
121+
ctx: this,
122+
listening: _listening
126123
});
127124

128-
if (listening) {
129-
var listeners = obj._listeners || (obj._listeners = {});
130-
listeners[listening.id] = listening;
125+
if (_listening) {
126+
var listeners = this._listeners || (this._listeners = {});
127+
listeners[_listening.id] = _listening;
128+
// Allow the listening to use a counter, instead of tracking
129+
// callbacks for library interop
130+
_listening.backbone = true;
131131
}
132132

133-
return obj;
133+
return this;
134134
};
135135

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

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

151-
// Bind callbacks on obj, and keep track of them on listening.
152-
internalOn(obj, name, callback, this, listening);
151+
// Bind callbacks on obj.
152+
var error = tryCatchOn(obj, name, callback, this);
153+
_listening = void 0;
154+
155+
if (error) throw error;
156+
// If the target obj is not Backbone.Events, track events manually.
157+
if (!listening.backbone) listening.on(name, callback);
158+
153159
return this;
154160
};
155161

@@ -165,16 +171,27 @@
165171
return events;
166172
};
167173

174+
// An try-catch guarded #on function, to prevent poisoning the global
175+
// `_listening` variable.
176+
var tryCatchOn = function(obj, name, callback, context) {
177+
try {
178+
obj.on(name, callback, context);
179+
} catch (e) {
180+
return e;
181+
}
182+
};
183+
168184
// Remove one or many callbacks. If `context` is null, removes all
169185
// callbacks with that function. If `callback` is null, removes all
170186
// callbacks for the event. If `name` is null, removes all bound
171187
// callbacks for all events.
172188
Events.off = function(name, callback, context) {
173189
if (!this._events) return this;
174190
this._events = eventsApi(offApi, this._events, name, callback, {
175-
context: context,
176-
listeners: this._listeners
191+
context: context,
192+
listeners: this._listeners
177193
});
194+
178195
return this;
179196
};
180197

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

187204
var ids = obj ? [obj._listenId] : _.keys(listeningTo);
188-
189205
for (var i = 0; i < ids.length; i++) {
190206
var listening = listeningTo[ids[i]];
191207

@@ -194,9 +210,8 @@
194210
if (!listening) break;
195211

196212
listening.obj.off(name, callback, this);
213+
if (!listening.backbone) listening.off(name, callback);
197214
}
198-
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
199-
200215
return this;
201216
};
202217

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

208-
var i = 0, length, listening;
209223
var context = options.context, listeners = options.listeners;
224+
var i = 0, names;
210225

211-
// Delete all events listeners and "drop" events.
212-
if (!name && !callback && !context) {
213-
var ids = _.keys(listeners);
214-
for (; i < ids.length; i++) {
215-
listening = listeners[ids[i]];
216-
delete listeners[listening.id];
217-
delete listening.listeningTo[listening.objId];
226+
// Delete all event listeners and "drop" events.
227+
if (!name && !context && !callback) {
228+
for (names = _.keys(listeners); i < names.length; i++) {
229+
listeners[names[i]].cleanup();
218230
}
219231
return;
220232
}
@@ -227,7 +239,7 @@
227239
// Bail out if there are no events stored.
228240
if (!handlers) break;
229241

230-
// Replace events if there are any remaining. Otherwise, clean up.
242+
// Find any remaining events.
231243
var remaining = [];
232244
for (var j = 0; j < handlers.length; j++) {
233245
var handler = handlers[j];
@@ -238,21 +250,19 @@
238250
) {
239251
remaining.push(handler);
240252
} else {
241-
listening = handler.listening;
242-
if (listening && --listening.count === 0) {
243-
delete listeners[listening.id];
244-
delete listening.listeningTo[listening.objId];
245-
}
253+
var listening = handler.listening;
254+
if (listening) listening.off(name, callback);
246255
}
247256
}
248257

249-
// Update tail event if the list has any events. Otherwise, clean up.
258+
// Replace events if there are any remaining. Otherwise, clean up.
250259
if (remaining.length) {
251260
events[name] = remaining;
252261
} else {
253262
delete events[name];
254263
}
255264
}
265+
256266
if (_.size(events)) return events;
257267
};
258268

@@ -327,6 +337,44 @@
327337
}
328338
};
329339

340+
// A listening class that tracks and cleans up memory bindings
341+
// when all callbacks have been offed.
342+
var Listening = function(listener, obj) {
343+
this.id = listener._listenId;
344+
this.listener = listener;
345+
this.obj = obj;
346+
this.backbone = false;
347+
this.count = 0;
348+
this._events = {};
349+
};
350+
351+
Listening.prototype.on = Events.on;
352+
353+
// Offs a callback (or several).
354+
// Uses an optimized counter if the listenee uses Backbone.Events.
355+
// Otherwise, falls back to manual tracking to support events
356+
// library interop.
357+
Listening.prototype.off = function(name, callback) {
358+
var cleanup;
359+
if (this.backbone) {
360+
this.count--;
361+
cleanup = this.count === 0;
362+
} else {
363+
this._events = eventsApi(offApi, this._events, name, callback, {
364+
context: void 0,
365+
listeners: void 0
366+
});
367+
cleanup = !this._events;
368+
}
369+
if (cleanup) this.cleanup();
370+
};
371+
372+
// Cleans up memory bindings between the listener and the listenee.
373+
Listening.prototype.cleanup = function() {
374+
delete this.listener._listeningTo[this.obj._listenId];
375+
if (this.backbone) delete this.obj._listeners[this.id];
376+
};
377+
330378
// Proxy Underscore methods to a Backbone class' prototype using a
331379
// particular attribute as the data argument
332380
var addMethod = function(length, method, attribute) {

0 commit comments

Comments
 (0)