Skip to content

Commit 6ef5746

Browse files
committed
Merge pull request #3594 from jridgewell/eventsApi-refinements
Refine eventsApi
2 parents 6d1128f + f2a2cd0 commit 6ef5746

File tree

2 files changed

+133
-123
lines changed

2 files changed

+133
-123
lines changed

backbone.js

Lines changed: 107 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -84,32 +84,48 @@
8484

8585
// Iterates over the standard `event, callback` (as well as the fancy multiple
8686
// 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;
9292
if (name && typeof name === 'object') {
9393
// 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);
9696
}
9797
} else if (name && eventSplitter.test(name)) {
9898
// Handle space separated event names.
9999
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);
101101
}
102102
} else {
103-
memo = iteratee(memo, name, callback, context, ctx);
103+
memo = iteratee(memo, name, callback, opts);
104104
}
105105
return memo;
106106
};
107107

108108
// Bind an event to a `callback` function. Passing `"all"` will bind
109109
// the callback to all events fired.
110110
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;
113129
};
114130

115131
// Inversion-of-control versions of `on`. Tell *this* object to listen to
@@ -123,23 +139,23 @@
123139
// This object is not listening to any other events on `obj` yet.
124140
// Setup the necessary references to track the listening callbacks.
125141
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};
130144
}
131145

132146
// 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);
135148
return this;
136149
};
137150

138151
// 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) {
140153
if (callback) {
141154
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 });
143159
}
144160
return events;
145161
};
@@ -150,129 +166,119 @@
150166
// callbacks for all events.
151167
Events.off = function(name, callback, context) {
152168
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+
});
172173
return this;
173174
};
174175

175176
// Tell this object to stop listening to either specific events ... or
176177
// to every object it's currently listening to.
177178
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+
180195
return this;
181196
};
182197

183198
// 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+
}
187216

188217
var names = name ? [name] : _.keys(events);
189-
for (var i = 0; i < names.length; i++) {
218+
for (; i < names.length; i++) {
190219
name = names[i];
191220
var handlers = events[name];
192221

193222
// Bail out if there are no events stored.
194223
if (!handlers) break;
195224

196-
// Find any remaining events.
225+
// Replace events if there are any remaining. Otherwise, clean up.
197226
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];
207240
}
208241
}
209242
}
210243

211-
// Replace events if there are any remaining. Otherwise, clean up.
244+
// Update tail event if the list has any events. Otherwise, clean up.
212245
if (remaining.length) {
213246
events[name] = remaining;
214247
} else {
215248
delete events[name];
216249
}
217250
}
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;
243252
};
244253

245254
// Bind an event to only be triggered a single time. After the first time
246255
// the callback is invoked, it will be removed. When multiple events are
247256
// passed in using the space-separated syntax, the event will fire once for every
248257
// event you passed in, not once for a combination of all events
249-
250258
Events.once = function(name, callback, context) {
251259
// 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));
253261
return this.on(events, void 0, context);
254262
};
255263

256264
// Inversion-of-control versions of `once`.
257265
Events.listenToOnce = function(obj, name, callback) {
258266
// 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));
260268
return this.listenTo(obj, events);
261269
};
262270

263271
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
264272
// `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;
276282
};
277283

278284
// Trigger one or many events, firing all bound callbacks. Callbacks are
@@ -286,19 +292,20 @@
286292
var args = Array(length);
287293
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
288294

289-
eventsApi(triggerApi, this, name, void 0, args);
295+
eventsApi(triggerApi, this._events, name, void 0, args);
290296
return this;
291297
};
292298

293299
// 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();
298305
if (events) triggerEvents(events, args);
299306
if (allEvents) triggerEvents(allEvents, [name].concat(args));
300307
}
301-
return obj;
308+
return objEvents;
302309
};
303310

304311
// A difficult-to-believe, but optimized internal dispatch function for

0 commit comments

Comments
 (0)