Skip to content

Commit d52f426

Browse files
JiaLiPassionmhevery
authored andcommitted
fix(platform-browser): run BLACK_LISTED_EVENTS outside of ngZone (angular#18993)
PR Close angular#18993
1 parent ed1175f commit d52f426

File tree

4 files changed

+186
-7
lines changed

4 files changed

+186
-7
lines changed

karma-js.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = function(config) {
3535
'node_modules/zone.js/dist/fake-async-test.js',
3636

3737
// Including systemjs because it defines `__eval`, which produces correct stack traces.
38+
'test-events.js',
3839
'shims_for_IE.js',
3940
'node_modules/systemjs/dist/system.src.js',
4041
{pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true},

packages/platform-browser/src/dom/events/dom_events.ts

+50-7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ const ANGULAR = 'ANGULAR';
3232
const NATIVE_ADD_LISTENER = 'addEventListener';
3333
const NATIVE_REMOVE_LISTENER = 'removeEventListener';
3434

35+
const blackListedEvents: string[] = Zone && Zone[__symbol__('BLACK_LISTED_EVENTS')];
36+
let blackListedMap: {[eventName: string]: string};
37+
if (blackListedEvents) {
38+
blackListedMap = {};
39+
blackListedEvents.forEach(eventName => { blackListedMap[eventName] = eventName; });
40+
}
41+
42+
const isBlackListedEvent = function(eventName: string) {
43+
if (!blackListedMap) {
44+
return false;
45+
}
46+
return blackListedMap.hasOwnProperty(eventName);
47+
};
48+
3549
interface TaskData {
3650
zone: any;
3751
handler: Function;
@@ -49,14 +63,29 @@ const globalListener = function(event: Event) {
4963
return;
5064
}
5165
const args: any = [event];
52-
taskDatas.forEach(taskData => {
66+
if (taskDatas.length === 1) {
67+
// if taskDatas only have one element, just invoke it
68+
const taskData = taskDatas[0];
5369
if (taskData.zone !== Zone.current) {
5470
// only use Zone.run when Zone.current not equals to stored zone
5571
return taskData.zone.run(taskData.handler, this, args);
5672
} else {
5773
return taskData.handler.apply(this, args);
5874
}
59-
});
75+
} else {
76+
// copy tasks as a snapshot to avoid event handlers remove
77+
// itself or others
78+
const copiedTasks = taskDatas.slice();
79+
for (let i = 0; i < copiedTasks.length; i++) {
80+
const taskData = copiedTasks[i];
81+
if (taskData.zone !== Zone.current) {
82+
// only use Zone.run when Zone.current not equals to stored zone
83+
taskData.zone.run(taskData.handler, this, args);
84+
} else {
85+
taskData.handler.apply(this, args);
86+
}
87+
}
88+
}
6089
};
6190

6291
@Injectable()
@@ -86,20 +115,34 @@ export class DomEventsPlugin extends EventManagerPlugin {
86115
let callback: EventListener = handler as EventListener;
87116
// if zonejs is loaded and current zone is not ngZone
88117
// we keep Zone.current on target for later restoration.
89-
if (zoneJsLoaded && !NgZone.isInAngularZone()) {
118+
if (zoneJsLoaded && (!NgZone.isInAngularZone() || isBlackListedEvent(eventName))) {
90119
let symbolName = symbolNames[eventName];
91120
if (!symbolName) {
92121
symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE);
93122
}
94123
let taskDatas: TaskData[] = (element as any)[symbolName];
95-
const listenerRegistered = taskDatas && taskDatas.length > 0;
124+
const globalListenerRegistered = taskDatas && taskDatas.length > 0;
96125
if (!taskDatas) {
97126
taskDatas = (element as any)[symbolName] = [];
98127
}
99-
if (taskDatas.filter(taskData => taskData.handler === callback).length === 0) {
100-
taskDatas.push({zone: Zone.current, handler: callback});
128+
129+
const zone = isBlackListedEvent(eventName) ? Zone.root : Zone.current;
130+
if (taskDatas.length === 0) {
131+
taskDatas.push({zone: zone, handler: callback});
132+
} else {
133+
let callbackRegistered = false;
134+
for (let i = 0; i < taskDatas.length; i++) {
135+
if (taskDatas[i].handler === callback) {
136+
callbackRegistered = true;
137+
break;
138+
}
139+
}
140+
if (!callbackRegistered) {
141+
taskDatas.push({zone: zone, handler: callback});
142+
}
101143
}
102-
if (!listenerRegistered) {
144+
145+
if (!globalListenerRegistered) {
103146
element[ADD_EVENT_LISTENER](eventName, globalListener, false);
104147
}
105148
} else {

packages/platform-browser/test/dom/events/event_manager_spec.ts

+126
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,132 @@ export function main() {
116116
getDOM().dispatchEvent(element, dispatchedEvent);
117117
expect(receivedEvent).toBe(null);
118118
});
119+
120+
it('should keep zone when addEventListener multiple times', () => {
121+
const Zone = (window as any)['Zone'];
122+
123+
const element = el('<div><div></div></div>');
124+
getDOM().appendChild(doc.body, element);
125+
const dispatchedEvent = getDOM().createMouseEvent('click');
126+
let receivedEvents: any[] /** TODO #9100 */ = [];
127+
let receivedZones: any[] = [];
128+
const handler1 = (e: any /** TODO #9100 */) => {
129+
receivedEvents.push(e);
130+
receivedZones.push(Zone.current.name);
131+
};
132+
const handler2 = (e: any /** TODO #9100 */) => {
133+
receivedEvents.push(e);
134+
receivedZones.push(Zone.current.name);
135+
};
136+
const manager = new EventManager([domEventPlugin], new FakeNgZone());
137+
138+
let remover1 = null;
139+
let remover2 = null;
140+
Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler1); });
141+
Zone.root.fork({name: 'test'}).run(() => {
142+
remover2 = manager.addEventListener(element, 'click', handler2);
143+
});
144+
getDOM().dispatchEvent(element, dispatchedEvent);
145+
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
146+
expect(receivedZones).toEqual([Zone.root.name, 'test']);
147+
148+
receivedEvents = [];
149+
remover1 && remover1();
150+
remover2 && remover2();
151+
getDOM().dispatchEvent(element, dispatchedEvent);
152+
expect(receivedEvents).toEqual([]);
153+
});
154+
155+
it('should handle event correctly when one handler remove itself ', () => {
156+
const Zone = (window as any)['Zone'];
157+
158+
const element = el('<div><div></div></div>');
159+
getDOM().appendChild(doc.body, element);
160+
const dispatchedEvent = getDOM().createMouseEvent('click');
161+
let receivedEvents: any[] /** TODO #9100 */ = [];
162+
let receivedZones: any[] = [];
163+
let remover1: any = null;
164+
let remover2: any = null;
165+
const handler1 = (e: any /** TODO #9100 */) => {
166+
receivedEvents.push(e);
167+
receivedZones.push(Zone.current.name);
168+
remover1 && remover1();
169+
};
170+
const handler2 = (e: any /** TODO #9100 */) => {
171+
receivedEvents.push(e);
172+
receivedZones.push(Zone.current.name);
173+
};
174+
const manager = new EventManager([domEventPlugin], new FakeNgZone());
175+
176+
Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler1); });
177+
Zone.root.fork({name: 'test'}).run(() => {
178+
remover2 = manager.addEventListener(element, 'click', handler2);
179+
});
180+
getDOM().dispatchEvent(element, dispatchedEvent);
181+
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
182+
expect(receivedZones).toEqual([Zone.root.name, 'test']);
183+
184+
receivedEvents = [];
185+
remover1 && remover1();
186+
remover2 && remover2();
187+
getDOM().dispatchEvent(element, dispatchedEvent);
188+
expect(receivedEvents).toEqual([]);
189+
});
190+
191+
it('should only add same callback once when addEventListener', () => {
192+
const Zone = (window as any)['Zone'];
193+
194+
const element = el('<div><div></div></div>');
195+
getDOM().appendChild(doc.body, element);
196+
const dispatchedEvent = getDOM().createMouseEvent('click');
197+
let receivedEvents: any[] /** TODO #9100 */ = [];
198+
let receivedZones: any[] = [];
199+
const handler = (e: any /** TODO #9100 */) => {
200+
receivedEvents.push(e);
201+
receivedZones.push(Zone.current.name);
202+
};
203+
const manager = new EventManager([domEventPlugin], new FakeNgZone());
204+
205+
let remover1 = null;
206+
let remover2 = null;
207+
Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler); });
208+
Zone.root.fork({name: 'test'}).run(() => {
209+
remover2 = manager.addEventListener(element, 'click', handler);
210+
});
211+
getDOM().dispatchEvent(element, dispatchedEvent);
212+
expect(receivedEvents).toEqual([dispatchedEvent]);
213+
expect(receivedZones).toEqual([Zone.root.name]);
214+
215+
receivedEvents = [];
216+
remover1 && remover1();
217+
remover2 && remover2();
218+
getDOM().dispatchEvent(element, dispatchedEvent);
219+
expect(receivedEvents).toEqual([]);
220+
});
221+
222+
it('should run blackListedEvents handler outside of ngZone', () => {
223+
const Zone = (window as any)['Zone'];
224+
const element = el('<div><div></div></div>');
225+
getDOM().appendChild(doc.body, element);
226+
const dispatchedEvent = getDOM().createMouseEvent('scroll');
227+
let receivedEvent: any /** TODO #9100 */ = null;
228+
let receivedZone: any = null;
229+
const handler = (e: any /** TODO #9100 */) => {
230+
receivedEvent = e;
231+
receivedZone = Zone.current;
232+
};
233+
const manager = new EventManager([domEventPlugin], new FakeNgZone());
234+
235+
let remover = manager.addEventListener(element, 'scroll', handler);
236+
getDOM().dispatchEvent(element, dispatchedEvent);
237+
expect(receivedEvent).toBe(dispatchedEvent);
238+
expect(receivedZone.name).toBe(Zone.root.name);
239+
240+
receivedEvent = null;
241+
remover && remover();
242+
getDOM().dispatchEvent(element, dispatchedEvent);
243+
expect(receivedEvent).toBe(null);
244+
});
119245
});
120246
}
121247

test-events.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
Zone[Zone.__symbol__('BLACK_LISTED_EVENTS')] = ['scroll'];

0 commit comments

Comments
 (0)