Bug 1834370 - Keep listeners for each event type in a separate array, and use binary search on the outer list. r=smaug

This considerably improves the testcase in bug 1834003, because it
reduces the amount of memory we need to look at when checking the
listeners at the nsWindowRoot. At the moment, nsWindowRoot has 156
listeners for 94 different event types, all from JSWindowActor event
listeners.

Having a separate array per event type also matches what Blink and Webkit do.

Differential Revision: https://phabricator.services.mozilla.com/D183431
This commit is contained in:
Markus Stange 2023-08-16 16:16:37 +00:00
Родитель 07ff9b4a2c
Коммит 5b48389e18
6 изменённых файлов: 727 добавлений и 348 удалений

Просмотреть файл

@ -956,7 +956,8 @@ bool nsContentUtils::IsExternalProtocol(nsIURI* aURI) {
return NS_SUCCEEDED(rv) && doesNotReturnData;
}
static nsAtom* GetEventTypeFromMessage(EventMessage aEventMessage) {
/* static */
nsAtom* nsContentUtils::GetEventTypeFromMessage(EventMessage aEventMessage) {
switch (aEventMessage) {
#define MESSAGE_TO_EVENT(name_, message_, type_, struct_) \
case message_: \

Просмотреть файл

@ -1692,6 +1692,11 @@ class nsContentUtils {
*/
static EventMessage GetEventMessage(nsAtom* aName);
/**
* Return the event type atom for a given event message.
*/
static nsAtom* GetEventTypeFromMessage(EventMessage aEventMessage);
/**
* Returns the EventMessage and nsAtom to be used for event listener
* registration.

Просмотреть файл

@ -9,6 +9,7 @@
#include "js/loader/LoadedScript.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/BinarySearch.h"
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/EventDispatcher.h"
@ -63,11 +64,6 @@ namespace mozilla {
using namespace dom;
using namespace hal;
#define EVENT_TYPE_EQUALS(ls, message, userType, allEvents) \
((ls->mEventMessage == message && \
(ls->mEventMessage != eUnidentifiedEvent || ls->mTypeAtom == userType)) || \
(allEvents && ls->mAllEvents))
static const uint32_t kAllMutationBits =
NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED |
NS_EVENT_BITS_MUTATION_NODEINSERTED | NS_EVENT_BITS_MUTATION_NODEREMOVED |
@ -98,6 +94,29 @@ static uint32_t MutationBitForEventType(EventMessage aEventType) {
return 0;
}
class ListenerMapEntryComparator {
public:
explicit ListenerMapEntryComparator(nsAtom* aTarget)
: mAddressOfEventType(reinterpret_cast<uintptr_t>(aTarget)) {}
int operator()(
const EventListenerManager::EventListenerMapEntry& aEntry) const {
uintptr_t value = reinterpret_cast<uintptr_t>(aEntry.mTypeAtom.get());
if (mAddressOfEventType == value) {
return 0;
}
if (mAddressOfEventType < value) {
return -1;
}
return 1;
}
private:
const uintptr_t mAddressOfEventType; // the address of the atom, can be 0
};
uint32_t EventListenerManager::sMainThreadCreatedCount = 0;
EventListenerManagerBase::EventListenerManagerBase()
@ -147,10 +166,19 @@ void EventListenerManager::RemoveAllListenersSilently() {
return;
}
mClearingListeners = true;
mListeners.Clear();
mListenerMap.Clear();
mClearingListeners = false;
}
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback,
EventListenerManager::EventListenerMap& aField, const char* aName,
uint32_t aFlags = 0) {
for (const auto& entry : aField.mEntries) {
ImplCycleCollectionTraverse(aCallback, *entry.mListeners, aName);
}
}
inline void ImplCycleCollectionTraverse(
nsCycleCollectionTraversalCallback& aCallback,
EventListenerManager::Listener& aField, const char* aName,
@ -179,7 +207,7 @@ inline void ImplCycleCollectionTraverse(
NS_IMPL_CYCLE_COLLECTION_CLASS(EventListenerManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EventListenerManager)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListeners)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerMap);
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EventListenerManager)
@ -210,6 +238,9 @@ void EventListenerManager::AddEventListenerInternal(
bool aAllEvents, AbortSignal* aSignal) {
MOZ_ASSERT((aEventMessage && aTypeAtom) || aAllEvents, // all-events listener
"Missing type");
MOZ_ASSERT_IF(
aEventMessage != eUnidentifiedEvent && !aAllEvents,
aTypeAtom == nsContentUtils::GetEventTypeFromMessage(aEventMessage));
if (!aListenerHolder || mClearingListeners) {
return;
@ -223,15 +254,15 @@ void EventListenerManager::AddEventListenerInternal(
// know that there's an EventListenerHolder on the stack holding a strong ref
// to the listener.
Listener* listener;
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; i++) {
listener = &mListeners.ElementAt(i);
RefPtr<ListenerArray> listeners =
aAllEvents ? mListenerMap.GetOrCreateListenersForAllEvents()
: mListenerMap.GetOrCreateListenersForType(aTypeAtom);
for (const Listener& listener : listeners->NonObservingRange()) {
// mListener == aListenerHolder is the last one, since it can be a bit slow.
if (listener->mListenerIsHandler == aHandler &&
listener->mFlags.EqualsForAddition(aFlags) &&
EVENT_TYPE_EQUALS(listener, aEventMessage, aTypeAtom, aAllEvents) &&
listener->mListener == aListenerHolder) {
if (listener.mListenerIsHandler == aHandler &&
listener.mFlags.EqualsForAddition(aFlags) &&
listener.mListener == aListenerHolder) {
return;
}
}
@ -239,8 +270,7 @@ void EventListenerManager::AddEventListenerInternal(
ClearNoListenersForEvents();
mNoListenerForEventAtom = nullptr;
listener =
aAllEvents ? mListeners.InsertElementAt(0) : mListeners.AppendElement();
Listener* listener = listeners->AppendElement();
listener->mEventMessage = aEventMessage;
listener->mTypeAtom = aTypeAtom;
listener->mFlags = aFlags;
@ -771,48 +801,41 @@ void EventListenerManager::RemoveEventListenerInternal(
return;
}
Listener* listener;
uint32_t count = mListeners.Length();
bool deviceType = IsDeviceType(aEventMessage);
RefPtr<EventListenerManager> kungFuDeathGrip(this);
for (uint32_t i = 0; i < count; ++i) {
listener = &mListeners.ElementAt(i);
if (EVENT_TYPE_EQUALS(listener, aEventMessage, aUserType, aAllEvents)) {
if (listener->mListener == aListenerHolder &&
listener->mFlags.EqualsForRemoval(aFlags)) {
mListeners.RemoveElementAt(i);
NotifyEventListenerRemoved(aUserType);
if (!aAllEvents && deviceType) {
DisableDevice(aEventMessage);
}
Maybe<size_t> entryIndex = aAllEvents
? mListenerMap.EntryIndexForAllEvents()
: mListenerMap.EntryIndexForType(aUserType);
if (!entryIndex) {
return;
}
ListenerArray& listenerArray = *mListenerMap.mEntries[*entryIndex].mListeners;
Maybe<uint32_t> listenerIndex = [&]() -> Maybe<uint32_t> {
uint32_t count = listenerArray.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &listenerArray.ElementAt(i);
if (listener->mListener == aListenerHolder &&
listener->mFlags.EqualsForRemoval(aFlags)) {
return Some(i);
}
}
}
return Nothing();
}();
bool EventListenerManager::Listener::MatchesEventMessage(
const WidgetEvent* aEvent, EventMessage aEventMessage) const {
MOZ_ASSERT(aEventMessage == aEvent->mMessage ||
aEventMessage == EventListenerManager::GetLegacyEventMessage(
aEvent->mMessage),
"aEvent and aEventMessage should agree, modulo legacyness");
if (MOZ_UNLIKELY(mAllEvents)) {
return true;
if (!listenerIndex) {
return;
}
// This is slightly different from EVENT_TYPE_EQUALS in that it returns
// true even when aEvent->mMessage == eUnidentifiedEvent and
// mEventMessage != eUnidentifiedEvent as long as the atoms are
// the same.
if (aEvent->mMessage == eUnidentifiedEvent) {
return mTypeAtom == aEvent->mSpecifiedEventType;
listenerArray.RemoveElementAt(*listenerIndex);
if (listenerArray.IsEmpty()) {
mListenerMap.mEntries.RemoveElementAt(*entryIndex);
}
RefPtr<EventListenerManager> kungFuDeathGrip(this);
NotifyEventListenerRemoved(aUserType);
if (!aAllEvents && IsDeviceType(aEventMessage)) {
DisableDevice(aEventMessage);
}
return mEventMessage == aEventMessage;
}
static bool IsDefaultPassiveWhenOnRoot(EventMessage aMessage) {
@ -885,15 +908,18 @@ void EventListenerManager::RemoveEventListenerByType(
}
EventListenerManager::Listener* EventListenerManager::FindEventHandler(
EventMessage aEventMessage, nsAtom* aTypeAtom) {
nsAtom* aTypeAtom) {
// Run through the listeners for this type and see if a script
// listener is registered
Listener* listener;
uint32_t count = mListeners.Length();
RefPtr<ListenerArray> listeners = mListenerMap.GetListenersForType(aTypeAtom);
if (!listeners) {
return nullptr;
}
uint32_t count = listeners->Length();
for (uint32_t i = 0; i < count; ++i) {
listener = &mListeners.ElementAt(i);
if (listener->mListenerIsHandler &&
EVENT_TYPE_EQUALS(listener, aEventMessage, aTypeAtom, false)) {
Listener* listener = &listeners->ElementAt(i);
if (listener->mListenerIsHandler) {
return listener;
}
}
@ -906,7 +932,7 @@ EventListenerManager::Listener* EventListenerManager::SetEventHandlerInternal(
MOZ_ASSERT(aName);
EventMessage eventMessage = GetEventMessage(aName);
Listener* listener = FindEventHandler(eventMessage, aName);
Listener* listener = FindEventHandler(aName);
if (!listener) {
// If we didn't find a script listener or no listeners existed
@ -921,7 +947,7 @@ EventListenerManager::Listener* EventListenerManager::SetEventHandlerInternal(
AddEventListenerInternal(EventListenerHolder(jsEventHandler), eventMessage,
aName, flags, true);
listener = FindEventHandler(eventMessage, aName);
listener = FindEventHandler(aName);
} else {
JSEventHandler* jsEventHandler = listener->GetJSEventHandler();
MOZ_ASSERT(jsEventHandler,
@ -1033,16 +1059,39 @@ void EventListenerManager::RemoveEventHandler(nsAtom* aName) {
return;
}
EventMessage eventMessage = GetEventMessage(aName);
Listener* listener = FindEventHandler(eventMessage, aName);
Maybe<size_t> entryIndex = mListenerMap.EntryIndexForType(aName);
if (!entryIndex) {
return;
}
if (listener) {
mListeners.RemoveElementAt(uint32_t(listener - &mListeners.ElementAt(0)));
ListenerArray& listenerArray = *mListenerMap.mEntries[*entryIndex].mListeners;
Maybe<uint32_t> listenerIndex = [&]() -> Maybe<uint32_t> {
uint32_t count = listenerArray.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &listenerArray.ElementAt(i);
if (listener->mListenerIsHandler) {
return Some(i);
}
}
return Nothing();
}();
if (!listenerIndex) {
return;
}
listenerArray.RemoveElementAt(*listenerIndex);
if (listenerArray.IsEmpty()) {
mListenerMap.mEntries.RemoveElementAt(*entryIndex);
}
RefPtr<EventListenerManager> kungFuDeathGrip(this);
NotifyEventListenerRemoved(aName);
EventMessage eventMessage = GetEventMessage(aName);
if (IsDeviceType(eventMessage)) {
DisableDevice(eventMessage);
}
}
}
nsresult EventListenerManager::CompileEventHandlerInternal(
@ -1366,6 +1415,66 @@ already_AddRefed<nsPIDOMWindowInner> EventListenerManager::WindowFromListener(
return innerWindow.forget();
}
Maybe<size_t> EventListenerManager::EventListenerMap::EntryIndexForType(
nsAtom* aTypeAtom) const {
MOZ_ASSERT(aTypeAtom);
size_t matchIndexOrInsertionPoint = 0;
bool foundMatch = BinarySearchIf(mEntries, 0, mEntries.Length(),
ListenerMapEntryComparator(aTypeAtom),
&matchIndexOrInsertionPoint);
return foundMatch ? Some(matchIndexOrInsertionPoint) : Nothing();
}
Maybe<size_t> EventListenerManager::EventListenerMap::EntryIndexForAllEvents()
const {
// If we have an entry for "all events listeners", it'll be at the beginning
// of the list and its type atom will be null.
return !mEntries.IsEmpty() && mEntries[0].mTypeAtom == nullptr ? Some(0)
: Nothing();
}
RefPtr<EventListenerManager::ListenerArray>
EventListenerManager::EventListenerMap::GetListenersForType(
nsAtom* aTypeAtom) const {
Maybe<size_t> index = EntryIndexForType(aTypeAtom);
return index ? mEntries[*index].mListeners : nullptr;
}
RefPtr<EventListenerManager::ListenerArray>
EventListenerManager::EventListenerMap::GetListenersForAllEvents() const {
Maybe<size_t> index = EntryIndexForAllEvents();
return index ? mEntries[*index].mListeners : nullptr;
}
RefPtr<EventListenerManager::ListenerArray>
EventListenerManager::EventListenerMap::GetOrCreateListenersForType(
nsAtom* aTypeAtom) {
MOZ_ASSERT(aTypeAtom);
size_t matchIndexOrInsertionPoint = 0;
bool foundMatch = BinarySearchIf(mEntries, 0, mEntries.Length(),
ListenerMapEntryComparator(aTypeAtom),
&matchIndexOrInsertionPoint);
if (foundMatch) {
return mEntries[matchIndexOrInsertionPoint].mListeners;
}
RefPtr<ListenerArray> listeners = MakeRefPtr<ListenerArray>();
mEntries.InsertElementAt(matchIndexOrInsertionPoint,
EventListenerMapEntry{aTypeAtom, listeners});
return listeners;
}
RefPtr<EventListenerManager::ListenerArray>
EventListenerManager::EventListenerMap::GetOrCreateListenersForAllEvents() {
RefPtr<ListenerArray> listeners = GetListenersForAllEvents();
if (!listeners) {
listeners = MakeRefPtr<ListenerArray>();
mEntries.InsertElementAt(0, EventListenerMapEntry{nullptr, listeners});
}
return listeners;
}
void EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
WidgetEvent* aEvent,
Event** aDOMEvent,
@ -1383,6 +1492,93 @@ void EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
aEvent->PreventDefault();
}
if (aEvent->mFlags.mImmediatePropagationStopped) {
return;
}
Maybe<AutoHandlingUserInputStatePusher> userInputStatePusher;
Maybe<AutoPopupStatePusher> popupStatePusher;
if (mIsMainThreadELM) {
userInputStatePusher.emplace(UserActivation::IsUserInteractionEvent(aEvent),
aEvent);
popupStatePusher.emplace(
PopupBlocker::GetEventPopupControlState(aEvent, *aDOMEvent));
}
EventMessage eventMessage = aEvent->mMessage;
RefPtr<nsAtom> typeAtom =
eventMessage == eUnidentifiedEvent
? aEvent->mSpecifiedEventType.get()
: nsContentUtils::GetEventTypeFromMessage(eventMessage);
if (!typeAtom) {
// Some messages don't have a corresponding type atom, e.g.
// eMouseEnterIntoWidget. These events can't have a listener, so we
// can stop here.
return;
}
bool hasAnyListenerForEventType = false;
// First, notify any "all events" listeners.
if (RefPtr<ListenerArray> listenersForAllEvents =
mListenerMap.GetListenersForAllEvents()) {
HandleEventWithListenerArray(listenersForAllEvents, typeAtom, eventMessage,
aPresContext, aEvent, aDOMEvent,
aCurrentTarget, aItemInShadowTree);
hasAnyListenerForEventType = true;
}
// Now look for listeners for typeAtom, and call them if we have any.
bool hasAnyListenerMatchingGroup = false;
if (RefPtr<ListenerArray> listeners =
mListenerMap.GetListenersForType(typeAtom)) {
hasAnyListenerMatchingGroup = HandleEventWithListenerArray(
listeners, typeAtom, eventMessage, aPresContext, aEvent, aDOMEvent,
aCurrentTarget, aItemInShadowTree);
hasAnyListenerForEventType = true;
}
if (!hasAnyListenerMatchingGroup && aEvent->IsTrusted()) {
// If we didn't find any matching listeners, and our event has a legacy
// version, check the listeners for the legacy version.
EventMessage legacyEventMessage = GetLegacyEventMessage(eventMessage);
if (legacyEventMessage != eventMessage) {
MOZ_ASSERT(
GetLegacyEventMessage(legacyEventMessage) == legacyEventMessage,
"Legacy event messages should not themselves have legacy versions");
RefPtr<nsAtom> legacyTypeAtom =
nsContentUtils::GetEventTypeFromMessage(legacyEventMessage);
if (RefPtr<ListenerArray> legacyListeners =
mListenerMap.GetListenersForType(legacyTypeAtom)) {
HandleEventWithListenerArray(
legacyListeners, legacyTypeAtom, legacyEventMessage, aPresContext,
aEvent, aDOMEvent, aCurrentTarget, aItemInShadowTree);
hasAnyListenerForEventType = true;
}
}
}
aEvent->mCurrentTarget = nullptr;
if (mIsMainThreadELM && !hasAnyListenerForEventType) {
if (aEvent->mMessage != eUnidentifiedEvent) {
mNoListenerForEvents[2] = mNoListenerForEvents[1];
mNoListenerForEvents[1] = mNoListenerForEvents[0];
mNoListenerForEvents[0] = aEvent->mMessage;
} else {
mNoListenerForEventAtom = aEvent->mSpecifiedEventType;
}
}
if (aEvent->DefaultPrevented()) {
*aEventStatus = nsEventStatus_eConsumeNoDefault;
}
}
bool EventListenerManager::HandleEventWithListenerArray(
ListenerArray* aListeners, nsAtom* aTypeAtom, EventMessage aEventMessage,
nsPresContext* aPresContext, WidgetEvent* aEvent, Event** aDOMEvent,
EventTarget* aCurrentTarget, bool aItemInShadowTree) {
auto ensureDOMEvent = [&]() {
if (!*aDOMEvent) {
// Lazily create the DOM event.
@ -1396,41 +1592,25 @@ void EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
return *aDOMEvent != nullptr;
};
Maybe<AutoHandlingUserInputStatePusher> userInputStatePusher;
Maybe<AutoPopupStatePusher> popupStatePusher;
if (mIsMainThreadELM) {
userInputStatePusher.emplace(UserActivation::IsUserInteractionEvent(aEvent),
aEvent);
popupStatePusher.emplace(
PopupBlocker::GetEventPopupControlState(aEvent, *aDOMEvent));
}
Maybe<EventMessageAutoOverride> eventMessageAutoOverride;
bool isOverridingEventMessage = aEvent->mMessage != aEventMessage;
bool hasAnyListenerMatchingGroup = false;
bool didReplaceOnceListener = false;
bool hasListener = false;
bool usingLegacyMessage = false;
bool hasRemovedListener = false;
EventMessage eventMessage = aEvent->mMessage;
while (!aEvent->mFlags.mImmediatePropagationStopped) {
Maybe<EventMessageAutoOverride> legacyAutoOverride;
bool hasListenerForCurrentGroup = false;
for (Listener& listenerRef : mListeners.EndLimitedRange()) {
for (Listener& listenerRef : aListeners->EndLimitedRange()) {
Listener* listener = &listenerRef;
if (!listener->MatchesEventMessage(aEvent, eventMessage)) {
continue;
}
if (listener->mListenerType == Listener::eNoListener) {
// The listener has been removed, it cannot handle anything.
// The listener is a placeholder value of a removed "once" listener.
continue;
}
if (!listener->mEnabled) {
// The listener has been disabled, for example by devtools.
continue;
}
hasListener = true;
if (!listener->MatchesEventGroup(aEvent)) {
continue;
}
hasListenerForCurrentGroup = true;
hasAnyListenerMatchingGroup = true;
// Check that the phase is same in event and event listener. Also check
// that the event is trusted or that the listener allows untrusted events.
@ -1446,14 +1626,14 @@ void EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
// called again inside the listener.
listenerHolder.emplace(std::move(*listener));
listener = listenerHolder.ptr();
hasRemovedListener = true;
didReplaceOnceListener = true;
}
if (ensureDOMEvent()) {
if (usingLegacyMessage && !legacyAutoOverride) {
if (isOverridingEventMessage && !eventMessageAutoOverride) {
// Override the domEvent's event-message (its .type) until we
// finish traversing listeners (when legacyAutoOverride
// destructs)
legacyAutoOverride.emplace(*aDOMEvent, eventMessage);
// finish traversing listeners (when eventMessageAutoOverride
// destructs).
eventMessageAutoOverride.emplace(*aDOMEvent, aEventMessage);
}
if (!HandleEventSingleListener(listener, aEvent, *aDOMEvent,
aCurrentTarget, aItemInShadowTree)) {
@ -1462,70 +1642,38 @@ void EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
}
}
// If we didn't find any matching listeners, and our event has a legacy
// version, we'll now switch to looking for that legacy version and we'll
// recheck our listeners.
if (hasListenerForCurrentGroup || usingLegacyMessage ||
!aEvent->IsTrusted()) {
// No need to recheck listeners, because we already found a match, we
// already rechecked them, or it is not a trusted event.
break;
}
EventMessage legacyEventMessage = GetLegacyEventMessage(eventMessage);
if (legacyEventMessage == eventMessage) {
break; // There's no legacy version of our event; no need to recheck.
}
MOZ_ASSERT(
GetLegacyEventMessage(legacyEventMessage) == legacyEventMessage,
"Legacy event messages should not themselves have legacy versions");
// Recheck our listeners, using the legacy event message we just looked up:
eventMessage = legacyEventMessage;
usingLegacyMessage = true;
}
aEvent->mCurrentTarget = nullptr;
if (hasRemovedListener) {
// If there are any once listeners replaced with a placeholder in
// the loop above, we need to clean up them here. Note that, this
// could clear once listeners handled in some outer level as well,
// but that should not affect the result.
mListeners.NonObservingRemoveElementsBy([](const Listener& aListener) {
if (didReplaceOnceListener) {
// If there are any once listeners replaced with a placeholder during the
// loop above, we need to clean up them here. Note that this could clear
// once listeners handled in some outer level as well, but that should not
// affect the result.
size_t oldLength = aListeners->Length();
aListeners->NonObservingRemoveElementsBy([](const Listener& aListener) {
return aListener.mListenerType == Listener::eNoListener;
});
NotifyEventListenerRemoved(aEvent->mSpecifiedEventType);
if (IsDeviceType(aEvent->mMessage)) {
// This is a device-type event, we need to check whether we can
// disable device after removing the once listeners.
const auto [begin, end] = mListeners.NonObservingRange();
const bool hasAnyListener =
std::any_of(begin, end, [aEvent](const Listener& listenerRef) {
const Listener* listener = &listenerRef;
return EVENT_TYPE_EQUALS(listener, aEvent->mMessage,
aEvent->mSpecifiedEventType,
/* all events */ false);
size_t newLength = aListeners->Length();
if (newLength == 0) {
// Remove the entry that has now become empty.
mListenerMap.mEntries.RemoveElementsBy([](EventListenerMapEntry& entry) {
return entry.mListeners->IsEmpty();
});
if (!hasAnyListener) {
DisableDevice(aEvent->mMessage);
}
if (newLength < oldLength) {
// Call NotifyEventListenerRemoved once for every removed listener.
size_t removedCount = oldLength - newLength;
for (size_t i = 0; i < removedCount; i++) {
NotifyEventListenerRemoved(aTypeAtom);
}
if (IsDeviceType(aEventMessage)) {
// Call DisableDevice once for every removed listener.
for (size_t i = 0; i < removedCount; i++) {
DisableDevice(aEventMessage);
}
}
}
}
if (mIsMainThreadELM && !hasListener) {
if (aEvent->mMessage != eUnidentifiedEvent) {
mNoListenerForEvents[2] = mNoListenerForEvents[1];
mNoListenerForEvents[1] = mNoListenerForEvents[0];
mNoListenerForEvents[0] = aEvent->mMessage;
} else {
mNoListenerForEventAtom = aEvent->mSpecifiedEventType;
}
}
if (aEvent->DefaultPrevented()) {
*aEventStatus = nsEventStatus_eConsumeNoDefault;
}
return hasAnyListenerMatchingGroup;
}
void EventListenerManager::Disconnect() {
@ -1616,11 +1764,10 @@ void EventListenerManager::RemoveListenerForAllEvents(
bool EventListenerManager::HasMutationListeners() {
if (mMayHaveMutationListeners) {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (listener->mEventMessage >= eLegacyMutationEventFirst &&
listener->mEventMessage <= eLegacyMutationEventLast) {
for (const auto& entry : mListenerMap.mEntries) {
EventMessage message = GetEventMessage(entry.mTypeAtom);
if (message >= eLegacyMutationEventFirst &&
message <= eLegacyMutationEventLast) {
return true;
}
}
@ -1632,15 +1779,14 @@ bool EventListenerManager::HasMutationListeners() {
uint32_t EventListenerManager::MutationListenerBits() {
uint32_t bits = 0;
if (mMayHaveMutationListeners) {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (listener->mEventMessage >= eLegacyMutationEventFirst &&
listener->mEventMessage <= eLegacyMutationEventLast) {
if (listener->mEventMessage == eLegacySubtreeModified) {
for (const auto& entry : mListenerMap.mEntries) {
EventMessage message = GetEventMessage(entry.mTypeAtom);
if (message >= eLegacyMutationEventFirst &&
message <= eLegacyMutationEventLast) {
if (message == eLegacySubtreeModified) {
return kAllMutationBits;
}
bits |= MutationBitForEventType(listener->mEventMessage);
bits |= MutationBitForEventType(message);
}
}
}
@ -1669,21 +1815,30 @@ bool EventListenerManager::HasListenersForInternal(
#endif
NS_ASSERTION(StringBeginsWith(name, u"on"_ns),
"Event name does not start with 'on'");
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
const Listener* listener = &mListeners.ElementAt(i);
if (listener->mTypeAtom == aEventNameWithOn) {
if (aIgnoreSystemGroup && listener->mFlags.mInSystemGroup) {
continue;
RefPtr<ListenerArray> listeners =
mListenerMap.GetListenersForType(aEventNameWithOn);
if (!listeners) {
return false;
}
MOZ_ASSERT(!listeners->IsEmpty());
if (!aIgnoreSystemGroup) {
return true;
}
// Check if any non-system-group listeners exist in `listeners`.
for (const auto& listener : listeners->NonObservingRange()) {
if (!listener.mFlags.mInSystemGroup) {
return true;
}
}
return false;
}
bool EventListenerManager::HasListeners() const {
return !mListeners.IsEmpty();
return !mListenerMap.IsEmpty();
}
nsresult EventListenerManager::GetListenerInfo(
@ -1691,7 +1846,8 @@ nsresult EventListenerManager::GetListenerInfo(
nsCOMPtr<EventTarget> target = mTarget;
NS_ENSURE_STATE(target);
aList.Clear();
for (const Listener& listener : mListeners.ForwardRange()) {
for (const auto& entry : mListenerMap.mEntries) {
for (const Listener& listener : entry.mListeners->ForwardRange()) {
// If this is a script handler and we haven't yet
// compiled the event handler itself go ahead and compile it
if (listener.mListenerType == Listener::eJSEventListener &&
@ -1705,7 +1861,7 @@ nsresult EventListenerManager::GetListenerInfo(
} else if (listener.mListenerType == Listener::eNoListener) {
continue;
} else {
eventType.Assign(Substring(nsDependentAtomString(listener.mTypeAtom), 2));
eventType.Assign(Substring(nsDependentAtomString(entry.mTypeAtom), 2));
}
JS::Rooted<JSObject*> callback(RootingCx());
@ -1722,7 +1878,8 @@ nsresult EventListenerManager::GetListenerInfo(
}
}
} else if (listener.mListenerType == Listener::eWebIDLListener) {
EventListener* listenerCallback = listener.mListener.GetWebIDLCallback();
EventListener* listenerCallback =
listener.mListener.GetWebIDLCallback();
callback = listenerCallback->CallbackOrNull();
callbackGlobal = listenerCallback->CallbackGlobalOrNull();
if (!callback) {
@ -1738,6 +1895,7 @@ nsresult EventListenerManager::GetListenerInfo(
listener.mListenerIsHandler);
aList.AppendElement(info.forget());
}
}
return NS_OK;
}
@ -1746,11 +1904,25 @@ EventListenerManager::Listener* EventListenerManager::GetListenerFor(
bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler) {
NS_ENSURE_TRUE(aListener, nullptr);
for (Listener& listener : mListeners.ForwardRange()) {
if ((aType.IsVoid() && !listener.mAllEvents) ||
!Substring(nsDependentAtomString(listener.mTypeAtom), 2)
.Equals(aType) ||
listener.mListenerType == Listener::eNoListener) {
RefPtr<ListenerArray> listeners;
if (aType.IsVoid()) {
listeners = mListenerMap.GetListenersForAllEvents();
} else {
for (auto& mapEntry : mListenerMap.mEntries) {
if (RefPtr<nsAtom> typeAtom = mapEntry.mTypeAtom) {
if (Substring(nsDependentAtomString(typeAtom), 2).Equals(aType)) {
listeners = mapEntry.mListeners;
break;
}
}
}
}
if (!listeners) {
return nullptr;
}
for (Listener& listener : listeners->ForwardRange()) {
if (listener.mListenerType == Listener::eNoListener) {
continue;
}
@ -1809,25 +1981,11 @@ nsresult EventListenerManager::SetListenerEnabled(
}
bool EventListenerManager::HasUnloadListeners() {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (listener->mEventMessage == eUnload) {
return true;
}
}
return false;
return mListenerMap.GetListenersForType(nsGkAtoms::onunload) != nullptr;
}
bool EventListenerManager::HasBeforeUnloadListeners() {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (listener->mEventMessage == eBeforeUnload) {
return true;
}
}
return false;
return mListenerMap.GetListenersForType(nsGkAtoms::onbeforeunload) != nullptr;
}
void EventListenerManager::SetEventHandler(nsAtom* aEventName,
@ -1875,8 +2033,7 @@ void EventListenerManager::SetEventHandler(
const TypedEventHandler* EventListenerManager::GetTypedEventHandler(
nsAtom* aEventName) {
EventMessage eventMessage = GetEventMessage(aEventName);
Listener* listener = FindEventHandler(eventMessage, aEventName);
Listener* listener = FindEventHandler(aEventName);
if (!listener) {
return nullptr;
@ -1895,12 +2052,29 @@ const TypedEventHandler* EventListenerManager::GetTypedEventHandler(
size_t EventListenerManager::SizeOfIncludingThis(
MallocSizeOf aMallocSizeOf) const {
return aMallocSizeOf(this) + mListenerMap.SizeOfExcludingThis(aMallocSizeOf);
}
size_t EventListenerManager::EventListenerMap::SizeOfExcludingThis(
MallocSizeOf aMallocSizeOf) const {
size_t n = mEntries.ShallowSizeOfExcludingThis(aMallocSizeOf);
for (const auto& entry : mEntries) {
n += entry.SizeOfExcludingThis(aMallocSizeOf);
}
return n;
}
size_t EventListenerManager::EventListenerMapEntry::SizeOfExcludingThis(
MallocSizeOf aMallocSizeOf) const {
return mListeners->SizeOfIncludingThis(aMallocSizeOf);
}
size_t EventListenerManager::ListenerArray::SizeOfIncludingThis(
MallocSizeOf aMallocSizeOf) const {
size_t n = aMallocSizeOf(this);
n += mListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
JSEventHandler* jsEventHandler =
mListeners.ElementAt(i).GetJSEventHandler();
n += ShallowSizeOfExcludingThis(aMallocSizeOf);
for (const auto& listener : NonObservingRange()) {
JSEventHandler* jsEventHandler = listener.GetJSEventHandler();
if (jsEventHandler) {
n += jsEventHandler->SizeOfIncludingThis(aMallocSizeOf);
}
@ -1908,10 +2082,17 @@ size_t EventListenerManager::SizeOfIncludingThis(
return n;
}
uint32_t EventListenerManager::ListenerCount() const {
uint32_t count = 0;
for (const auto& entry : mListenerMap.mEntries) {
count += entry.mListeners->Length();
}
return count;
}
void EventListenerManager::MarkForCC() {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
const Listener& listener = mListeners.ElementAt(i);
for (const auto& entry : mListenerMap.mEntries) {
for (const auto& listener : entry.mListeners->NonObservingRange()) {
JSEventHandler* jsEventHandler = listener.GetJSEventHandler();
if (jsEventHandler) {
const TypedEventHandler& typedHandler =
@ -1923,15 +2104,15 @@ void EventListenerManager::MarkForCC() {
listener.mListener.GetWebIDLCallback()->MarkForCC();
}
}
}
if (mRefCnt.IsPurple()) {
mRefCnt.RemovePurple();
}
}
void EventListenerManager::TraceListeners(JSTracer* aTrc) {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
const Listener& listener = mListeners.ElementAt(i);
for (const auto& entry : mListenerMap.mEntries) {
for (const auto& listener : entry.mListeners->NonObservingRange()) {
JSEventHandler* jsEventHandler = listener.GetJSEventHandler();
if (jsEventHandler) {
const TypedEventHandler& typedHandler =
@ -1940,52 +2121,65 @@ void EventListenerManager::TraceListeners(JSTracer* aTrc) {
mozilla::TraceScriptHolder(typedHandler.Ptr(), aTrc);
}
} else if (listener.mListenerType == Listener::eWebIDLListener) {
mozilla::TraceScriptHolder(listener.mListener.GetWebIDLCallback(), aTrc);
mozilla::TraceScriptHolder(listener.mListener.GetWebIDLCallback(),
aTrc);
}
// We might have eWrappedJSListener, but that is the legacy type for
// JS implemented event listeners, and trickier to handle here.
}
}
}
bool EventListenerManager::HasNonSystemGroupListenersForUntrustedKeyEvents() {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (!listener->mFlags.mInSystemGroup &&
listener->mFlags.mAllowUntrustedEvents &&
(listener->mTypeAtom == nsGkAtoms::onkeydown ||
listener->mTypeAtom == nsGkAtoms::onkeypress ||
listener->mTypeAtom == nsGkAtoms::onkeyup)) {
for (const auto& entry : mListenerMap.mEntries) {
if (entry.mTypeAtom != nsGkAtoms::onkeydown &&
entry.mTypeAtom != nsGkAtoms::onkeypress &&
entry.mTypeAtom != nsGkAtoms::onkeyup) {
continue;
}
for (const auto& listener : entry.mListeners->NonObservingRange()) {
if (!listener.mFlags.mInSystemGroup &&
listener.mFlags.mAllowUntrustedEvents) {
return true;
}
}
}
return false;
}
bool EventListenerManager::
HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents() {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (!listener->mFlags.mPassive && !listener->mFlags.mInSystemGroup &&
listener->mFlags.mAllowUntrustedEvents &&
(listener->mTypeAtom == nsGkAtoms::onkeydown ||
listener->mTypeAtom == nsGkAtoms::onkeypress ||
listener->mTypeAtom == nsGkAtoms::onkeyup)) {
for (const auto& entry : mListenerMap.mEntries) {
if (entry.mTypeAtom != nsGkAtoms::onkeydown &&
entry.mTypeAtom != nsGkAtoms::onkeypress &&
entry.mTypeAtom != nsGkAtoms::onkeyup) {
continue;
}
for (const auto& listener : entry.mListeners->NonObservingRange()) {
if (!listener.mFlags.mPassive && !listener.mFlags.mInSystemGroup &&
listener.mFlags.mAllowUntrustedEvents) {
return true;
}
}
}
return false;
}
bool EventListenerManager::HasApzAwareListeners() {
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (IsApzAwareListener(listener)) {
if (!mIsMainThreadELM) {
return false;
}
for (const auto& entry : mListenerMap.mEntries) {
if (!IsApzAwareEvent(entry.mTypeAtom)) {
continue;
}
for (const auto& listener : entry.mListeners->NonObservingRange()) {
if (!listener.mFlags.mPassive) {
return true;
}
}
}
return false;
}
@ -2022,27 +2216,34 @@ bool EventListenerManager::IsApzAwareEvent(nsAtom* aEvent) {
bool EventListenerManager::HasNonPassiveWheelListener() {
MOZ_ASSERT(NS_IsMainThread());
uint32_t count = mListeners.Length();
for (uint32_t i = 0; i < count; ++i) {
Listener* listener = &mListeners.ElementAt(i);
if (!listener->mFlags.mPassive && IsWheelEventType(listener->mTypeAtom)) {
for (const auto& entry : mListenerMap.mEntries) {
if (!IsWheelEventType(entry.mTypeAtom)) {
continue;
}
for (const auto& listener : entry.mListeners->NonObservingRange()) {
if (!listener.mFlags.mPassive) {
return true;
}
}
}
return false;
}
void EventListenerManager::RemoveAllListeners() {
while (!mListeners.IsEmpty()) {
size_t idx = mListeners.Length() - 1;
RefPtr<nsAtom> type = mListeners.ElementAt(idx).mTypeAtom;
EventMessage message = mListeners.ElementAt(idx).mEventMessage;
mListeners.RemoveElementAt(idx);
for (auto& entry : mListenerMap.mEntries) {
RefPtr<nsAtom> type = entry.mTypeAtom;
ListenerArray& listeners = *entry.mListeners;
while (!listeners.IsEmpty()) {
size_t idx = listeners.Length() - 1;
EventMessage message = listeners.ElementAt(idx).mEventMessage;
listeners.RemoveElementAt(idx);
NotifyEventListenerRemoved(type);
if (IsDeviceType(message)) {
DisableDevice(message);
}
}
}
mListenerMap.Clear();
}
already_AddRefed<nsIScriptGlobalObject>

Просмотреть файл

@ -271,9 +271,6 @@ class EventListenerManager final : public EventListenerManagerBase {
}
}
MOZ_ALWAYS_INLINE bool MatchesEventMessage(
const WidgetEvent* aEvent, EventMessage aEventMessage) const;
MOZ_ALWAYS_INLINE bool MatchesEventGroup(const WidgetEvent* aEvent) const {
return mFlags.mInSystemGroup == aEvent->mFlags.mInSystemGroup;
}
@ -291,6 +288,66 @@ class EventListenerManager final : public EventListenerManagerBase {
}
};
/**
* A reference counted subclass of a listener observer array.
*/
struct ListenerArray final : public nsAutoTObserverArray<Listener, 1> {
NS_INLINE_DECL_REFCOUNTING(EventListenerManager::ListenerArray);
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
protected:
~ListenerArray() = default;
};
/**
* An entry in the event listener map for a certain event type, carrying the
* array of listeners for that type.
*/
struct EventListenerMapEntry {
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
// The event type. Null if this entry is for "all events" listeners.
RefPtr<nsAtom> mTypeAtom;
// The array of listeners. New listeners are always added at the end.
// This is a RefPtr rather than an inline member for two reasons:
// - It needs to be a separate heap allocation so that, if the array of
// entries is mutated during iteration, the ListenerArray remains in a
// stable place.
// - It's a RefPtr rather than a UniquePtr so that iteration can share
// ownership of it and make sure that the listener array remains alive
// even if the entry is removed during iteration.
RefPtr<ListenerArray> mListeners;
};
/**
* The map of event listeners, keyed by event type atom.
*/
struct EventListenerMap {
bool IsEmpty() const { return mEntries.IsEmpty(); }
void Clear() { mEntries.Clear(); }
Maybe<size_t> EntryIndexForType(nsAtom* aTypeAtom) const;
Maybe<size_t> EntryIndexForAllEvents() const;
// Returns null if no entry is present for the given type.
RefPtr<ListenerArray> GetListenersForType(nsAtom* aTypeAtom) const;
RefPtr<ListenerArray> GetListenersForAllEvents() const;
// Never returns null, creates a new empty entry if needed.
RefPtr<ListenerArray> GetOrCreateListenersForType(nsAtom* aTypeAtom);
RefPtr<ListenerArray> GetOrCreateListenersForAllEvents();
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
// The array of entries, ordered by event type atom (specifically by the
// nsAtom* address). If mEntries contains an entry for "all events"
// listeners, that entry will be the first entry, because its atom will be
// null so it will be ordered to the front.
// All entries have non-empty listener arrays. If a non-empty listener
// entry becomes empty, it is removed immediately.
AutoTArray<EventListenerMapEntry, 2> mEntries;
};
explicit EventListenerManager(dom::EventTarget* aTarget);
NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EventListenerManager)
@ -405,7 +462,7 @@ class EventListenerManager final : public EventListenerManagerBase {
return;
}
if (mListeners.IsEmpty() || aEvent->PropagationStopped()) {
if (mListenerMap.IsEmpty() || aEvent->PropagationStopped()) {
return;
}
@ -513,7 +570,7 @@ class EventListenerManager final : public EventListenerManagerBase {
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
uint32_t ListenerCount() const { return mListeners.Length(); }
uint32_t ListenerCount() const;
void MarkForCC();
@ -543,6 +600,17 @@ class EventListenerManager final : public EventListenerManagerBase {
dom::EventTarget* aCurrentTarget,
nsEventStatus* aEventStatus, bool aItemInShadowTree);
/**
* Iterate the listener array and calls the matching listeners.
*
* Returns true if any listener matching the event group was found.
*/
MOZ_CAN_RUN_SCRIPT
bool HandleEventWithListenerArray(
ListenerArray* aListeners, nsAtom* aTypeAtom, EventMessage aEventMessage,
nsPresContext* aPresContext, WidgetEvent* aEvent, dom::Event** aDOMEvent,
dom::EventTarget* aCurrentTarget, bool aItemInShadowTree);
/**
* Call the listener.
*
@ -588,7 +656,7 @@ class EventListenerManager final : public EventListenerManagerBase {
/**
* Find the Listener for the "inline" event listener for aTypeAtom.
*/
Listener* FindEventHandler(EventMessage aEventMessage, nsAtom* aTypeAtom);
Listener* FindEventHandler(nsAtom* aTypeAtom);
/**
* Set the "inline" event listener for aName to aHandler. aHandler may be
@ -691,7 +759,7 @@ class EventListenerManager final : public EventListenerManagerBase {
// BE AWARE, a lot of instances of EventListenerManager will be created.
// Therefor, we need to keep this class compact. When you add integer
// members, please add them to EventListemerManagerBase and check the size
// members, please add them to EventListenerManagerBase and check the size
// at build time.
already_AddRefed<nsIScriptGlobalObject> GetScriptGlobalAndDocument(
@ -699,7 +767,7 @@ class EventListenerManager final : public EventListenerManagerBase {
void MaybeMarkPassive(EventMessage aMessage, EventListenerFlags& aFlags);
nsAutoTObserverArray<Listener, 2> mListeners;
EventListenerMap mListenerMap;
dom::EventTarget* MOZ_NON_OWNING_REF mTarget;
RefPtr<nsAtom> mNoListenerForEventAtom;

Просмотреть файл

@ -78,7 +78,16 @@ function runTests() {
root.addEventListener("click", bubblingListener);
root.addEventListener("fooevent", capturingListener, true);
root.addEventListener("fooevent", bubblingListener);
infos = els.getListenerInfoFor(root);
// We now have both "click" and "fooevent" listeners.
// Get the new set of listener infos, because we'll want to flip certain
// "click" event listeners on and off in the tests below.
// The order of event types is not guaranteed by getListenerInfoFor; but the
// order of listeners for a single event is guaranteed. So we filter the infos
// by event type.
const combinedListenerInfos = [...els.getListenerInfoFor(root)];
const clickInfos = combinedListenerInfos.filter((info) => info.type == "click");
// Use a child node to dispatch events so that both capturing and bubbling
// listeners get called.
l2 = document.getElementById("testlevel2");
@ -88,29 +97,29 @@ function runTests() {
ok(bubblingListenerCalled);
clearListenerStates();
infos[0].enabled = false;
clickInfos[0].enabled = false;
l2.click();
ok(!handlerCalled);
ok(capturingListenerCalled);
ok(bubblingListenerCalled);
clearListenerStates();
infos[0].enabled = true;
clickInfos[0].enabled = true;
infos[1].enabled = false;
clickInfos[1].enabled = false;
l2.click();
ok(handlerCalled);
ok(!capturingListenerCalled);
ok(bubblingListenerCalled);
clearListenerStates();
infos[1].enabled = true;
clickInfos[1].enabled = true;
infos[2].enabled = false;
clickInfos[2].enabled = false;
l2.click();
ok(handlerCalled);
ok(capturingListenerCalled);
ok(!bubblingListenerCalled);
clearListenerStates();
infos[2].enabled = true;
clickInfos[2].enabled = true;
root.removeEventListener("click", capturingListener, true);
root.removeEventListener("click", bubblingListener);

Просмотреть файл

@ -0,0 +1,95 @@
<!doctype html>
<title>Various edge cases where listeners are removed during iteration</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
test(function() {
var type = "foo";
var target = document.createElement("div");
var listener1CallCount = 0;
var listener2CallCount = 0;
var listener3CallCount = 0;
function listener1() {
listener1CallCount++;
target.removeEventListener(type, listener1);
target.removeEventListener(type, listener2);
target.addEventListener(type, listener3);
}
function listener2() {
listener2CallCount++;
}
function listener3() {
listener3CallCount++;
}
target.addEventListener(type, listener1);
target.addEventListener(type, listener2);
// Dispatch the event. Only listener1 should be called because
// it removes listener2. And listener3 is added when we've already
// started iterating, so it shouldn't be called either.
target.dispatchEvent(new Event(type));
assert_equals(listener1CallCount, 1);
assert_equals(listener2CallCount, 0);
assert_equals(listener3CallCount, 0);
// Now that only listener3 is set, dispatch another event. Only
// listener3 should be called.
target.dispatchEvent(new Event(type));
assert_equals(listener1CallCount, 1);
assert_equals(listener2CallCount, 0);
assert_equals(listener3CallCount, 1);
}, "Removing all listeners and then adding a new one should work.");
test(function() {
var type = "foo";
var target = document.createElement("div");
var listener1CallCount = 0;
var listener2CallCount = 0;
var listener3CallCount = 0;
function listener1() {
listener1CallCount++;
// Recursively dispatch another event from this listener.
// This will only call listener2 because listener1 is a "once" listener.
target.dispatchEvent(new Event(type));
assert_equals(listener1CallCount, 1);
assert_equals(listener2CallCount, 1);
assert_equals(listener3CallCount, 0);
// Now all listeners are removed - the two "once" listeners have already both
// been called once. Add another listener.
target.addEventListener(type, listener3);
}
function listener2() {
listener2CallCount++;
}
function listener3() {
listener3CallCount++;
}
// Add two "once" listeners.
target.addEventListener(type, listener1, { once: true });
target.addEventListener(type, listener2, { once: true });
// Dispatch the event.
target.dispatchEvent(new Event(type));
// The listener call counts should still match what they were
// at the end of listener1.
assert_equals(listener1CallCount, 1);
assert_equals(listener2CallCount, 1);
assert_equals(listener3CallCount, 0);
// Now that only listener3 is set, dispatch another event. Only
// listener3 should be called.
target.dispatchEvent(new Event(type));
assert_equals(listener1CallCount, 1);
assert_equals(listener2CallCount, 1);
assert_equals(listener3CallCount, 1);
}, "Nested usage of once listeners should work.");
</script>