Skip to content

Commit

Permalink
Merge branch 'eventsApi-refactor' into listenTo-memory-leak
Browse files Browse the repository at this point in the history
  • Loading branch information
jridgewell committed Jan 26, 2015
2 parents 75bf3c8 + bbeeb24 commit 6ad0063
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 125 deletions.
194 changes: 88 additions & 106 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,32 +81,24 @@
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
on: function(name, callback, context) {
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
this._events || (this._events = {});
onApi(this._events, name, callback, context, this);
this._events = eventsApi(onApi, this._events || {}, name, callback, context, this);
return this;
},

// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, it will be removed.
once: function(name, callback, context) {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
var self = this;
var once = _.once(function() {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.on(name, once, context);
name = onceMap(name, callback, _.bind(this.off, this));
return this.on(name, callback, context);
},

// 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
// callbacks for all events.
off: function(name, callback, context) {
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
this._events = offApi(this._events, name, callback, context);
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, context);
return this;
},

Expand All @@ -117,51 +109,44 @@
trigger: function(name) {
if (!this._events) return this;
var args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
eventsApi(triggerApi, this, name, triggerApi, args);
return this;
},

// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
listenTo: function(obj, name, callback) {
if (!listenApi(this, 'listenTo', obj, name, callback)) return this;
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
listeningTo[id] || (listeningTo[id] = {obj: obj, events: {}});
var listenee = listeningTo[id] || (listeningTo[id] = {obj: obj, events: {}});
obj.on(name, callback, this);
onApi(listeningTo[id].events, name, callback);
listenee.events = eventsApi(onApi, listenee.events, name, callback);
return this;
},

listenToOnce: function(obj, name, callback) {
if (!listenApi(this, 'listenToOnce', obj, name, callback) || !callback) return this;
var once = _.once(function() {
this.stopListening(obj, name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.listenTo(obj, name, once);
name = onceMap(name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, name, callback);
},

// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening: function(obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo || !listenApi(this, 'stopListening', obj, name, callback)) return this;
if (!listeningTo) return this;

var listeneeIds = (obj) ? [obj._listenId] : _.keys(listeningTo);
for (var i = 0, length = listeneeIds.length; i < length; i++) {
var listenee = listeningTo[listeneeIds[i]];
if (!listenee) continue;
var id = listeneeIds[i];
var listenee = listeningTo[id];
if (!listenee) break;
listenee.obj.off(name, callback, this);
var events = offApi(listenee.events, name, callback);
if (!events) delete this._listeningTo[listeneeIds[i]];
var events = eventsApi(offApi, listenee.events, name, callback);
if (!events) delete listeningTo[id];
}
if (_.isEmpty(this._listeningTo)) this._listeningTo = void 0;
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
}

Expand All @@ -173,102 +158,99 @@
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;

// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
var eventsApi = function(api, events, name, callback, context, ctx) {
var i = 0, names, length;
if (name && typeof name === 'object') {
// Handle event maps.
for (names = _.keys(name), length = names.length; i < length; i++) {
events = api(events, names[i], name[names[i]], context, ctx);
}
return false;
}

// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, length = names.length; i < length; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
} else if (name && eventSplitter.test(name)) {
// Handle space separated event names.
for (names = name.split(eventSplitter), length = names.length; i < length; i++) {
events = api(events, names[i], callback, context, ctx);
}
return false;
}

return true;
};

// Implement the fancy Events API features for the inversion-of-control
// methods.
var listenApi = function(obj, action, other, name, callback) {
if (!name) return true;

// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action](other, key, name[key]);
}
return false;
}

// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, length = names.length; i < length; i++) {
obj[action](other, names[i], callback);
}
return false;
} else {
events = api(events, name, callback, context, ctx);
}

return true;
return events;
};

// Handles actually adding the event handler.
var onApi = function(events, name, callback, context, ctx) {
events = events[name] || (events[name] = []);
events.push({callback: callback, context: context, ctx: context || ctx});
if (callback) {
var handlers = events[name] || [];
events[name] = handlers.concat({callback: callback, context: context, ctx: context || ctx});
}
return events;
};

// Handles removing any event handlers that are no longer wanted.
var offApi = function(events, name, callback, context) {
// Remove all callbacks for all events.
if (!name && !callback && !context) {
return;
}

var names = name ? [name] : _.keys(events);
for (var i = 0, length = names.length; i < length; i++) {
name = names[i];
// Remove all callbacks for all events.
if (!events || !name && !context && !callback) return;

// Bail out if there are no events stored.
var handlers = events[name];
if (!handlers) continue;
var names = name ? [name] : _.keys(events);
for (var i = 0, length = names.length; i < length; i++) {
name = names[i];

// Remove all handlers for this event.
if (!callback && !context) {
delete events[name];
continue;
}
// Bail out if there are no events stored.
var handlers = events[name];
if (!handlers) continue;

// Find any remaining events.
var remaining = [];
// Find any remaining events.
var remaining = [];
if (callback || context) {
for (var j = 0, k = handlers.length; j < k; j++) {
var handler = handlers[j];
if (
callback && callback !== handler.callback &&
callback !== handler.callback._callback ||
context && context !== handler.context
callback !== handler.callback._callback ||
context && context !== handler.context
) {
remaining.push(handler);
}
}
}

// Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
// Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
return _.isEmpty(events) ? void 0 : events;
}
return _.isEmpty(events) ? void 0 : events;
};

var triggerApi = function(obj, name, sentinel, args) {
if (obj._events) {
if (sentinel !== triggerApi) args = [sentinel].concat(args);
var events = obj._events[name];
var allEvents = obj._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return obj;
};

// Maps the normalized event callbacks into onceWrappers.
var onceMap = function(name, callback, offer) {
return eventsApi(function(map, name, callback, offer) {
if (callback) map[name] = onceWrap(name, callback, offer);
return map;
}, {}, name, callback, offer);
};

// Wraps an event callback, using the `offer` function to off the event
// once it's been called.
var onceWrap = function(name, callback, offer) {
var once = _.once(function() {
offer(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return once;
};

// A difficult-to-believe, but optimized internal dispatch function for
Expand Down
43 changes: 24 additions & 19 deletions test/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,54 +15,61 @@
equal(obj.counter, 5, 'counter should be incremented five times.');
});

test("binding and triggering multiple events", 4, function() {
test("binding and triggering multiple events", 9, function() {
var obj = { counter: 0 };
_.extend(obj, Backbone.Events);
var arg = {};

obj.on('a b c', function() { obj.counter += 1; });
obj.on('a b c', function(x) {
obj.counter += 1;
strictEqual(x, arg);
});

obj.trigger('a');
obj.trigger('a', arg);
equal(obj.counter, 1);

obj.trigger('a b');
obj.trigger('a b', arg);
equal(obj.counter, 3);

obj.trigger('c');
obj.trigger('c', arg);
equal(obj.counter, 4);

obj.off('a c');
obj.trigger('a b c');
obj.trigger('a b c', arg);
equal(obj.counter, 5);
});

test("binding and triggering with event maps", function() {
test("binding and triggering with event maps", 14, function() {
var obj = { counter: 0 };
_.extend(obj, Backbone.Events);

var increment = function() {
var increment = function(x, y) {
this.counter += 1;
strictEqual(x, arg);
strictEqual(y, arg2);
};
var arg = {}, arg2 = {};

obj.on({
a: increment,
b: increment,
c: increment
}, obj);

obj.trigger('a');
obj.trigger({ a: arg }, arg2);
equal(obj.counter, 1);

obj.trigger('a b');
obj.trigger({ a: arg, b: arg }, arg2);
equal(obj.counter, 3);

obj.trigger('c');
obj.trigger({ c: arg }, arg2);
equal(obj.counter, 4);

obj.off({
a: increment,
c: increment
}, obj);
obj.trigger('a b c');
obj.trigger({ a: arg, b: arg, c: arg }, arg2);
equal(obj.counter, 5);
});

Expand Down Expand Up @@ -343,14 +350,12 @@
test("callback list is not altered during trigger", 2, function () {
var counter = 0, obj = _.extend({}, Backbone.Events);
var incr = function(){ counter++; };
obj.on('event', function(){ obj.on('event', incr).on('all', incr); })
.trigger('event');
var incrOn = function(){ obj.on('event', incr).on('all', incr); };
var incrOff = function(){ obj.off('event', incr).off('all', incr); };
obj.on('all', incrOn).on('event', incrOn).trigger('event');
equal(counter, 0, 'bind does not alter callback list');
obj.off()
.on('event', function(){ obj.off('event', incr).off('all', incr); })
.on('event', incr)
.on('all', incr)
.trigger('event');
obj.off().on('all', incrOff).on('event', incrOff)
.on('event', incr).on('all', incr).trigger('event');
equal(counter, 2, 'unbind does not alter callback list');
});

Expand Down

0 comments on commit 6ad0063

Please sign in to comment.