diff --git a/src/ol/events/eventtarget.js b/src/ol/events/eventtarget.js index f2591ef538..81ac20f993 100644 --- a/src/ol/events/eventtarget.js +++ b/src/ol/events/eventtarget.js @@ -1,5 +1,6 @@ goog.provide('ol.events.EventTarget'); +goog.require('goog.asserts'); goog.require('ol.Disposable'); goog.require('ol.events'); goog.require('ol.events.Event'); @@ -26,6 +27,12 @@ ol.events.EventTarget = function() { goog.base(this); + /** + * @private + * @type {!Object.} + */ + this.pendingRemovals_ = {}; + /** * @private * @type {!Object.>} @@ -46,7 +53,7 @@ ol.events.EventTarget.prototype.addEventListener = function(type, listener) { listeners = this.listeners_[type] = []; } if (listeners.indexOf(listener) === -1) { - listeners.unshift(listener); + listeners.push(listener); } }; @@ -64,12 +71,17 @@ ol.events.EventTarget.prototype.dispatchEvent = function(event) { evt.target = this; var listeners = this.listeners_[type]; if (listeners) { - for (var i = listeners.length - 1; i >= 0; --i) { - if (listeners[i].call(this, evt) === false || - evt.propagationStopped) { + this.pendingRemovals_[type] = 0; + for (var i = 0, ii = listeners.length; i < ii; ++i) { + if (listeners[i].call(this, evt) === false || evt.propagationStopped) { return false; } } + var pendingRemovals = this.pendingRemovals_[type]; + delete this.pendingRemovals_[type]; + while (pendingRemovals--) { + this.removeEventListener(type, ol.nullFunction); + } } }; @@ -84,7 +96,7 @@ ol.events.EventTarget.prototype.disposeInternal = function() { /** * Get the listeners for a specified event type. Listeners are returned in the - * opposite order that they will be called in. + * order that they will be called in. * * @param {string} type Type. * @return {Array.} Listeners. @@ -114,9 +126,16 @@ ol.events.EventTarget.prototype.removeEventListener = function(type, listener) { var listeners = this.listeners_[type]; if (listeners) { var index = listeners.indexOf(listener); - listeners.splice(index, 1); - if (listeners.length === 0) { - delete this.listeners_[type]; + goog.asserts.assert(index != -1, 'listener not found'); + if (type in this.pendingRemovals_) { + // make listener a no-op, and remove later in #dispatchEvent() + listeners[index] = ol.nullFunction; + ++this.pendingRemovals_[type]; + } else { + listeners.splice(index, 1); + if (listeners.length === 0) { + delete this.listeners_[type]; + } } } }; diff --git a/test/spec/ol/events.test.js b/test/spec/ol/events.test.js index 6a9f79dd4c..30f6f5f944 100644 --- a/test/spec/ol/events.test.js +++ b/test/spec/ol/events.test.js @@ -214,7 +214,7 @@ describe('ol.events', function() { var key2 = ol.events.listen(target, 'foo', listener, {}); expect(key1.boundListener).to.not.equal(key2.boundListener); expect(target.getListeners('foo')).to.eql( - [key2.boundListener, key1.boundListener]); + [key1.boundListener, key2.boundListener]); }); }); diff --git a/test/spec/ol/events/eventtarget.test.js b/test/spec/ol/events/eventtarget.test.js index 9fd6619a20..af5cbc3add 100644 --- a/test/spec/ol/events/eventtarget.test.js +++ b/test/spec/ol/events/eventtarget.test.js @@ -115,6 +115,21 @@ describe('ol.events.EventTarget', function() { expect(events[0]).to.equal(event); expect(events[0].target).to.equal(eventTarget); }); + it('is safe to remove listeners in listeners', function() { + eventTarget.addEventListener('foo', spy3); + eventTarget.addEventListener('foo', function() { + eventTarget.removeEventListener('foo', spy1); + eventTarget.removeEventListener('foo', spy2); + eventTarget.removeEventListener('foo', spy3); + }); + eventTarget.addEventListener('foo', spy1); + eventTarget.addEventListener('foo', spy2); + expect(function() { + eventTarget.dispatchEvent('foo'); + }).not.to.throwException(); + expect(called).to.eql([3]); + expect(eventTarget.getListeners('foo')).to.have.length(1); + }); }); describe('#dispose()', function() {