|
87 | 87 | // Regular expression used to split event strings. |
88 | 88 | var eventSplitter = /\s+/; |
89 | 89 |
|
| 90 | + // A private global variable to share between listeners and listenees. |
| 91 | + var _listening; |
| 92 | + |
90 | 93 | // Iterates over the standard `event, callback` (as well as the fancy multiple |
91 | 94 | // space-separated events `"change blur", callback` and jQuery-style event |
92 | 95 | // maps `{event: callback}`), reducing them by manipulating `memo`. |
|
96 | 99 | var i = 0, names; |
97 | 100 | if (name && typeof name === 'object') { |
98 | 101 | // Handle event maps. |
99 | | - for (names = _.keys(name); i < names.length ; i++) { |
| 102 | + for (names = _.keys(name); i < names.length; i++) { |
100 | 103 | memo = iteratee(memo, names[i], name[names[i]], opts); |
101 | 104 | } |
102 | 105 | } else if (name && eventSplitter.test(name)) { |
|
113 | 116 | // Bind an event to a `callback` function. Passing `"all"` will bind |
114 | 117 | // the callback to all events fired. |
115 | 118 | 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 |
126 | 123 | }); |
127 | 124 |
|
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; |
131 | 131 | } |
132 | 132 |
|
133 | | - return obj; |
| 133 | + return this; |
134 | 134 | }; |
135 | 135 |
|
136 | 136 | // Inversion-of-control versions of `on`. Tell *this* object to listen to |
137 | 137 | // 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) { |
139 | 139 | if (!obj) return this; |
140 | 140 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); |
141 | 141 | var listeningTo = this._listeningTo || (this._listeningTo = {}); |
142 | | - var listening = listeningTo[id]; |
| 142 | + var listening = _listening = listeningTo[id]; |
143 | 143 |
|
144 | 144 | // This object is not listening to any other events on `obj` yet. |
145 | 145 | // Setup the necessary references to track the listening callbacks. |
146 | 146 | 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); |
149 | 149 | } |
150 | 150 |
|
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 | + |
153 | 159 | return this; |
154 | 160 | }; |
155 | 161 |
|
|
165 | 171 | return events; |
166 | 172 | }; |
167 | 173 |
|
| 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 | + |
168 | 184 | // Remove one or many callbacks. If `context` is null, removes all |
169 | 185 | // callbacks with that function. If `callback` is null, removes all |
170 | 186 | // callbacks for the event. If `name` is null, removes all bound |
171 | 187 | // callbacks for all events. |
172 | 188 | Events.off = function(name, callback, context) { |
173 | 189 | if (!this._events) return this; |
174 | 190 | this._events = eventsApi(offApi, this._events, name, callback, { |
175 | | - context: context, |
176 | | - listeners: this._listeners |
| 191 | + context: context, |
| 192 | + listeners: this._listeners |
177 | 193 | }); |
| 194 | + |
178 | 195 | return this; |
179 | 196 | }; |
180 | 197 |
|
|
185 | 202 | if (!listeningTo) return this; |
186 | 203 |
|
187 | 204 | var ids = obj ? [obj._listenId] : _.keys(listeningTo); |
188 | | - |
189 | 205 | for (var i = 0; i < ids.length; i++) { |
190 | 206 | var listening = listeningTo[ids[i]]; |
191 | 207 |
|
|
194 | 210 | if (!listening) break; |
195 | 211 |
|
196 | 212 | listening.obj.off(name, callback, this); |
| 213 | + if (!listening.backbone) listening.off(name, callback); |
197 | 214 | } |
198 | | - if (_.isEmpty(listeningTo)) this._listeningTo = void 0; |
199 | | - |
200 | 215 | return this; |
201 | 216 | }; |
202 | 217 |
|
|
205 | 220 | // No events to consider. |
206 | 221 | if (!events) return; |
207 | 222 |
|
208 | | - var i = 0, length, listening; |
209 | 223 | var context = options.context, listeners = options.listeners; |
| 224 | + var i = 0, names; |
210 | 225 |
|
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(); |
218 | 230 | } |
219 | 231 | return; |
220 | 232 | } |
|
227 | 239 | // Bail out if there are no events stored. |
228 | 240 | if (!handlers) break; |
229 | 241 |
|
230 | | - // Replace events if there are any remaining. Otherwise, clean up. |
| 242 | + // Find any remaining events. |
231 | 243 | var remaining = []; |
232 | 244 | for (var j = 0; j < handlers.length; j++) { |
233 | 245 | var handler = handlers[j]; |
|
238 | 250 | ) { |
239 | 251 | remaining.push(handler); |
240 | 252 | } 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); |
246 | 255 | } |
247 | 256 | } |
248 | 257 |
|
249 | | - // Update tail event if the list has any events. Otherwise, clean up. |
| 258 | + // Replace events if there are any remaining. Otherwise, clean up. |
250 | 259 | if (remaining.length) { |
251 | 260 | events[name] = remaining; |
252 | 261 | } else { |
253 | 262 | delete events[name]; |
254 | 263 | } |
255 | 264 | } |
| 265 | + |
256 | 266 | if (_.size(events)) return events; |
257 | 267 | }; |
258 | 268 |
|
|
327 | 337 | } |
328 | 338 | }; |
329 | 339 |
|
| 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 | + |
330 | 378 | // Proxy Underscore methods to a Backbone class' prototype using a |
331 | 379 | // particular attribute as the data argument |
332 | 380 | var addMethod = function(length, method, attribute) { |
|
0 commit comments