diff --git a/packages/core/src/render3/instructions/listener.ts b/packages/core/src/render3/instructions/listener.ts index 24de43559d..96f53e2357 100644 --- a/packages/core/src/render3/instructions/listener.ts +++ b/packages/core/src/render3/instructions/listener.ts @@ -149,9 +149,13 @@ function listenerInternal( existingListener = findExistingListener(lView, eventName, tNode.index); } if (existingListener !== null) { - // Attach a new listener at the head of the coalesced listeners list. - (listenerFn).__ngNextListenerFn__ = (existingListener).__ngNextListenerFn__; - (existingListener).__ngNextListenerFn__ = listenerFn; + // Attach a new listener to coalesced listeners list, maintaining the order in which + // listeners are registered. For performance reasons, we keep a reference to the last + // listener in that list (in `__ngLastListenerFn__` field), so we can avoid going through + // the entire set each time we need to add a new listener. + const lastListenerFn = (existingListener).__ngLastListenerFn__ || existingListener; + lastListenerFn.__ngNextListenerFn__ = listenerFn; + (existingListener).__ngLastListenerFn__ = listenerFn; processOutputs = false; } else { // The first argument of `listen` function in Procedural Renderer is: diff --git a/packages/core/test/acceptance/listener_spec.ts b/packages/core/test/acceptance/listener_spec.ts index 52dfca3b00..4dc39d9bba 100644 --- a/packages/core/test/acceptance/listener_spec.ts +++ b/packages/core/test/acceptance/listener_spec.ts @@ -238,5 +238,38 @@ describe('event listeners', () => { expect(componentInstance.count).toEqual(1); expect(componentInstance.someValue).toEqual(42); }); + + it('should maintain the order in which listeners are registered', () => { + const log: string[] = []; + @Component({ + selector: 'my-comp', + template: '', + }) + class MyComp { + counter = 0; + count() { log.push('component.click'); } + } + + @Directive({selector: '[dirA]'}) + class DirA { + @HostListener('click') + count() { log.push('dirA.click'); } + } + + @Directive({selector: '[dirB]'}) + class DirB { + @HostListener('click') + count() { log.push('dirB.click'); } + } + + TestBed.configureTestingModule({declarations: [MyComp, DirA, DirB]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const button = fixture.nativeElement.firstChild; + button.click(); + + expect(log).toEqual(['dirA.click', 'dirB.click', 'component.click']); + }); }); });