|
84 | 84 |
|
85 | 85 | // Iterates over the standard `event, callback` (as well as the fancy multiple |
86 | 86 | // space-separated events `"change blur", callback` and jQuery-style event |
87 | | - // maps `{event: callback}`), reducing them by manipulating `events`. |
88 | | - // Passes a normalized (single event name and callback), as well as the `context` |
89 | | - // and `ctx` arguments to `iteratee`. |
90 | | - var eventsApi = function(iteratee, memo, name, callback, context, ctx) { |
91 | | - var i = 0, names, length; |
| 87 | + // maps `{event: callback}`), reducing them by manipulating `memo`. |
| 88 | + // Passes a normalized single event name and callback, as well as any |
| 89 | + // optional `opts`. |
| 90 | + var eventsApi = function(iteratee, memo, name, callback, opts) { |
| 91 | + var i = 0, names; |
92 | 92 | if (name && typeof name === 'object') { |
93 | 93 | // Handle event maps. |
94 | | - for (names = _.keys(name); i < names.length; i++) { |
95 | | - memo = iteratee(memo, names[i], name[names[i]], context, ctx); |
| 94 | + for (names = _.keys(name); i < names.length ; i++) { |
| 95 | + memo = iteratee(memo, names[i], name[names[i]], opts); |
96 | 96 | } |
97 | 97 | } else if (name && eventSplitter.test(name)) { |
98 | 98 | // Handle space separated event names. |
99 | 99 | for (names = name.split(eventSplitter); i < names.length; i++) { |
100 | | - memo = iteratee(memo, names[i], callback, context, ctx); |
| 100 | + memo = iteratee(memo, names[i], callback, opts); |
101 | 101 | } |
102 | 102 | } else { |
103 | | - memo = iteratee(memo, name, callback, context, ctx); |
| 103 | + memo = iteratee(memo, name, callback, opts); |
104 | 104 | } |
105 | 105 | return memo; |
106 | 106 | }; |
107 | 107 |
|
108 | 108 | // Bind an event to a `callback` function. Passing `"all"` will bind |
109 | 109 | // the callback to all events fired. |
110 | 110 | Events.on = function(name, callback, context) { |
111 | | - this._events = eventsApi(onApi, this._events || {}, name, callback, context, this); |
112 | | - return this; |
| 111 | + return internalOn(this, name, callback, context); |
| 112 | + }; |
| 113 | + |
| 114 | + // An internal use `on` function, used to guard the `listening` argument from |
| 115 | + // the public API. |
| 116 | + var internalOn = function(obj, name, callback, context, listening) { |
| 117 | + obj._events = eventsApi(onApi, obj._events || {}, name, callback, { |
| 118 | + context: context, |
| 119 | + ctx: obj, |
| 120 | + listening: listening |
| 121 | + }); |
| 122 | + |
| 123 | + if (listening) { |
| 124 | + var listeners = obj._listeners || (obj._listeners = {}); |
| 125 | + listeners[listening.id] = listening; |
| 126 | + } |
| 127 | + |
| 128 | + return obj; |
113 | 129 | }; |
114 | 130 |
|
115 | 131 | // Inversion-of-control versions of `on`. Tell *this* object to listen to |
|
123 | 139 | // This object is not listening to any other events on `obj` yet. |
124 | 140 | // Setup the necessary references to track the listening callbacks. |
125 | 141 | if (!listening) { |
126 | | - listening = listeningTo[id] = {obj: obj, events: {}}; |
127 | | - id = this._listenId || (this._listenId = _.uniqueId('l')); |
128 | | - var listeners = obj._listeners || (obj._listeners = {}); |
129 | | - listeners[id] = this; |
| 142 | + var thisId = this._listenId || (this._listenId = _.uniqueId('l')); |
| 143 | + listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; |
130 | 144 | } |
131 | 145 |
|
132 | 146 | // Bind callbacks on obj, and keep track of them on listening. |
133 | | - obj.on(name, callback, this); |
134 | | - listening.events = eventsApi(onApi, listening.events, name, callback); |
| 147 | + internalOn(obj, name, callback, this, listening); |
135 | 148 | return this; |
136 | 149 | }; |
137 | 150 |
|
138 | 151 | // The reducing API that adds a callback to the `events` object. |
139 | | - var onApi = function(events, name, callback, context, ctx) { |
| 152 | + var onApi = function(events, name, callback, options) { |
140 | 153 | if (callback) { |
141 | 154 | var handlers = events[name] || (events[name] = []); |
142 | | - handlers.push({callback: callback, context: context, ctx: context || ctx}); |
| 155 | + var context = options.context, ctx = options.ctx, listening = options.listening; |
| 156 | + if (listening) listening.count++; |
| 157 | + |
| 158 | + handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); |
143 | 159 | } |
144 | 160 | return events; |
145 | 161 | }; |
|
150 | 166 | // callbacks for all events. |
151 | 167 | Events.off = function(name, callback, context) { |
152 | 168 | if (!this._events) return this; |
153 | | - this._events = eventsApi(offApi, this._events, name, callback, context); |
154 | | - |
155 | | - var listeners = this._listeners; |
156 | | - if (listeners) { |
157 | | - // Listeners always bind themselves as the context, so if `context` |
158 | | - // is passed, narrow down the search to just that listener. |
159 | | - var ids = context != null ? [context._listenId] : _.keys(listeners); |
160 | | - |
161 | | - for (var i = 0; i < ids.length; i++) { |
162 | | - var listener = listeners[ids[i]]; |
163 | | - |
164 | | - // Bail out if listener isn't listening. |
165 | | - if (!listener) break; |
166 | | - |
167 | | - // Tell each listener to stop, without infinitely calling `#off`. |
168 | | - internalStopListening(listener, this, name, callback); |
169 | | - } |
170 | | - if (_.isEmpty(listeners)) this._listeners = void 0; |
171 | | - } |
| 169 | + this._events = eventsApi(offApi, this._events, name, callback, { |
| 170 | + context: context, |
| 171 | + listeners: this._listeners |
| 172 | + }); |
172 | 173 | return this; |
173 | 174 | }; |
174 | 175 |
|
175 | 176 | // Tell this object to stop listening to either specific events ... or |
176 | 177 | // to every object it's currently listening to. |
177 | 178 | Events.stopListening = function(obj, name, callback) { |
178 | | - // Use an internal stopListening, telling it to call off on `obj`. |
179 | | - if (this._listeningTo) internalStopListening(this, obj, name, callback, true); |
| 179 | + var listeningTo = this._listeningTo; |
| 180 | + if (!listeningTo) return this; |
| 181 | + |
| 182 | + var ids = obj ? [obj._listenId] : _.keys(listeningTo); |
| 183 | + |
| 184 | + for (var i = 0; i < ids.length; i++) { |
| 185 | + var listening = listeningTo[ids[i]]; |
| 186 | + |
| 187 | + // If listening doesn't exist, this object is not currently |
| 188 | + // listening to obj. Break out early. |
| 189 | + if (!listening) break; |
| 190 | + |
| 191 | + listening.obj.off(name, callback, this); |
| 192 | + } |
| 193 | + if (_.isEmpty(listeningTo)) this._listeningTo = void 0; |
| 194 | + |
180 | 195 | return this; |
181 | 196 | }; |
182 | 197 |
|
183 | 198 | // The reducing API that removes a callback from the `events` object. |
184 | | - var offApi = function(events, name, callback, context) { |
185 | | - // Remove all callbacks for all events. |
186 | | - if (!events || !name && !context && !callback) return; |
| 199 | + var offApi = function(events, name, callback, options) { |
| 200 | + // No events to consider. |
| 201 | + if (!events) return; |
| 202 | + |
| 203 | + var i = 0, length, listening; |
| 204 | + var context = options.context, listeners = options.listeners; |
| 205 | + |
| 206 | + // Delete all events listeners and "drop" events. |
| 207 | + if (!name && !callback && !context) { |
| 208 | + var ids = _.keys(listeners); |
| 209 | + for (; i < ids.length; i++) { |
| 210 | + listening = listeners[ids[i]]; |
| 211 | + delete listeners[listening.id]; |
| 212 | + delete listening.listeningTo[listening.objId]; |
| 213 | + } |
| 214 | + return; |
| 215 | + } |
187 | 216 |
|
188 | 217 | var names = name ? [name] : _.keys(events); |
189 | | - for (var i = 0; i < names.length; i++) { |
| 218 | + for (; i < names.length; i++) { |
190 | 219 | name = names[i]; |
191 | 220 | var handlers = events[name]; |
192 | 221 |
|
193 | 222 | // Bail out if there are no events stored. |
194 | 223 | if (!handlers) break; |
195 | 224 |
|
196 | | - // Find any remaining events. |
| 225 | + // Replace events if there are any remaining. Otherwise, clean up. |
197 | 226 | var remaining = []; |
198 | | - if (callback || context) { |
199 | | - for (var j = 0, k = handlers.length; j < k; j++) { |
200 | | - var handler = handlers[j]; |
201 | | - if ( |
202 | | - callback && callback !== handler.callback && |
203 | | - callback !== handler.callback._callback || |
204 | | - context && context !== handler.context |
205 | | - ) { |
206 | | - remaining.push(handler); |
| 227 | + for (var j = 0; j < handlers.length; j++) { |
| 228 | + var handler = handlers[j]; |
| 229 | + if ( |
| 230 | + callback && callback !== handler.callback && |
| 231 | + callback !== handler.callback._callback || |
| 232 | + context && context !== handler.context |
| 233 | + ) { |
| 234 | + remaining.push(handler); |
| 235 | + } else { |
| 236 | + listening = handler.listening; |
| 237 | + if (listening && --listening.count === 0) { |
| 238 | + delete listeners[listening.id]; |
| 239 | + delete listening.listeningTo[listening.objId]; |
207 | 240 | } |
208 | 241 | } |
209 | 242 | } |
210 | 243 |
|
211 | | - // Replace events if there are any remaining. Otherwise, clean up. |
| 244 | + // Update tail event if the list has any events. Otherwise, clean up. |
212 | 245 | if (remaining.length) { |
213 | 246 | events[name] = remaining; |
214 | 247 | } else { |
215 | 248 | delete events[name]; |
216 | 249 | } |
217 | 250 | } |
218 | | - if (!_.isEmpty(events)) return events; |
219 | | - }; |
220 | | - |
221 | | - var internalStopListening = function(listener, obj, name, callback, offEvents) { |
222 | | - var listeningTo = listener._listeningTo; |
223 | | - var ids = obj ? [obj._listenId] : _.keys(listeningTo); |
224 | | - for (var i = 0; i < ids.length; i++) { |
225 | | - var id = ids[i]; |
226 | | - var listening = listeningTo[id]; |
227 | | - |
228 | | - // If listening doesn't exist, this object is not currently |
229 | | - // listening to obj. Break out early. |
230 | | - if (!listening) break; |
231 | | - obj = listening.obj; |
232 | | - if (offEvents) obj._events = eventsApi(offApi, obj._events, name, callback, listener); |
233 | | - |
234 | | - // Events will only ever be falsey if all the event callbacks |
235 | | - // are removed. If so, stop delete the listening. |
236 | | - var events = eventsApi(offApi, listening.events, name, callback); |
237 | | - if (!events) { |
238 | | - delete listeningTo[id]; |
239 | | - delete listening.obj._listeners[listener._listenId]; |
240 | | - } |
241 | | - } |
242 | | - if (_.isEmpty(listeningTo)) listener._listeningTo = void 0; |
| 251 | + if (_.size(events)) return events; |
243 | 252 | }; |
244 | 253 |
|
245 | 254 | // Bind an event to only be triggered a single time. After the first time |
246 | 255 | // the callback is invoked, it will be removed. When multiple events are |
247 | 256 | // passed in using the space-separated syntax, the event will fire once for every |
248 | 257 | // event you passed in, not once for a combination of all events |
249 | | - |
250 | 258 | Events.once = function(name, callback, context) { |
251 | 259 | // Map the event into a `{event: once}` object. |
252 | | - var events = onceMap(name, callback, _.bind(this.off, this)); |
| 260 | + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); |
253 | 261 | return this.on(events, void 0, context); |
254 | 262 | }; |
255 | 263 |
|
256 | 264 | // Inversion-of-control versions of `once`. |
257 | 265 | Events.listenToOnce = function(obj, name, callback) { |
258 | 266 | // Map the event into a `{event: once}` object. |
259 | | - var events = onceMap(name, callback, _.bind(this.stopListening, this, obj)); |
| 267 | + var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); |
260 | 268 | return this.listenTo(obj, events); |
261 | 269 | }; |
262 | 270 |
|
263 | 271 | // Reduces the event callbacks into a map of `{event: onceWrapper}`. |
264 | 272 | // `offer` unbinds the `onceWrapper` after it as been called. |
265 | | - var onceMap = function(name, callback, offer) { |
266 | | - return eventsApi(function(map, name, callback, offer) { |
267 | | - if (callback) { |
268 | | - var once = map[name] = _.once(function() { |
269 | | - offer(name, once); |
270 | | - callback.apply(this, arguments); |
271 | | - }); |
272 | | - once._callback = callback; |
273 | | - } |
274 | | - return map; |
275 | | - }, {}, name, callback, offer); |
| 273 | + var onceMap = function(map, name, callback, offer) { |
| 274 | + if (callback) { |
| 275 | + var once = map[name] = _.once(function() { |
| 276 | + offer(name, once); |
| 277 | + callback.apply(this, arguments); |
| 278 | + }); |
| 279 | + once._callback = callback; |
| 280 | + } |
| 281 | + return map; |
276 | 282 | }; |
277 | 283 |
|
278 | 284 | // Trigger one or many events, firing all bound callbacks. Callbacks are |
|
286 | 292 | var args = Array(length); |
287 | 293 | for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; |
288 | 294 |
|
289 | | - eventsApi(triggerApi, this, name, void 0, args); |
| 295 | + eventsApi(triggerApi, this._events, name, void 0, args); |
290 | 296 | return this; |
291 | 297 | }; |
292 | 298 |
|
293 | 299 | // Handles triggering the appropriate event callbacks. |
294 | | - var triggerApi = function(obj, name, cb, args) { |
295 | | - if (obj._events) { |
296 | | - var events = obj._events[name]; |
297 | | - var allEvents = obj._events.all; |
| 300 | + var triggerApi = function(objEvents, name, cb, args) { |
| 301 | + if (objEvents) { |
| 302 | + var events = objEvents[name]; |
| 303 | + var allEvents = objEvents.all; |
| 304 | + if (events && allEvents) allEvents = allEvents.slice(); |
298 | 305 | if (events) triggerEvents(events, args); |
299 | 306 | if (allEvents) triggerEvents(allEvents, [name].concat(args)); |
300 | 307 | } |
301 | | - return obj; |
| 308 | + return objEvents; |
302 | 309 | }; |
303 | 310 |
|
304 | 311 | // A difficult-to-believe, but optimized internal dispatch function for |
|
0 commit comments