fix(platform-browser): run BLACK_LISTED_EVENTS outside of ngZone (#18993)

PR Close #18993
This commit is contained in:
JiaLi.Passion 2017-09-02 02:30:37 +09:00 committed by Miško Hevery
parent ed1175f27e
commit d52f42688a
4 changed files with 186 additions and 7 deletions

View File

@ -35,6 +35,7 @@ module.exports = function(config) {
'node_modules/zone.js/dist/fake-async-test.js', 'node_modules/zone.js/dist/fake-async-test.js',
// Including systemjs because it defines `__eval`, which produces correct stack traces. // Including systemjs because it defines `__eval`, which produces correct stack traces.
'test-events.js',
'shims_for_IE.js', 'shims_for_IE.js',
'node_modules/systemjs/dist/system.src.js', 'node_modules/systemjs/dist/system.src.js',
{pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true}, {pattern: 'node_modules/rxjs/**', included: false, watched: false, served: true},

View File

@ -32,6 +32,20 @@ const ANGULAR = 'ANGULAR';
const NATIVE_ADD_LISTENER = 'addEventListener'; const NATIVE_ADD_LISTENER = 'addEventListener';
const NATIVE_REMOVE_LISTENER = 'removeEventListener'; const NATIVE_REMOVE_LISTENER = 'removeEventListener';
const blackListedEvents: string[] = Zone && Zone[__symbol__('BLACK_LISTED_EVENTS')];
let blackListedMap: {[eventName: string]: string};
if (blackListedEvents) {
blackListedMap = {};
blackListedEvents.forEach(eventName => { blackListedMap[eventName] = eventName; });
}
const isBlackListedEvent = function(eventName: string) {
if (!blackListedMap) {
return false;
}
return blackListedMap.hasOwnProperty(eventName);
};
interface TaskData { interface TaskData {
zone: any; zone: any;
handler: Function; handler: Function;
@ -49,14 +63,29 @@ const globalListener = function(event: Event) {
return; return;
} }
const args: any = [event]; const args: any = [event];
taskDatas.forEach(taskData => { if (taskDatas.length === 1) {
// if taskDatas only have one element, just invoke it
const taskData = taskDatas[0];
if (taskData.zone !== Zone.current) { if (taskData.zone !== Zone.current) {
// only use Zone.run when Zone.current not equals to stored zone // only use Zone.run when Zone.current not equals to stored zone
return taskData.zone.run(taskData.handler, this, args); return taskData.zone.run(taskData.handler, this, args);
} else { } else {
return taskData.handler.apply(this, args); return taskData.handler.apply(this, args);
} }
}); } else {
// copy tasks as a snapshot to avoid event handlers remove
// itself or others
const copiedTasks = taskDatas.slice();
for (let i = 0; i < copiedTasks.length; i++) {
const taskData = copiedTasks[i];
if (taskData.zone !== Zone.current) {
// only use Zone.run when Zone.current not equals to stored zone
taskData.zone.run(taskData.handler, this, args);
} else {
taskData.handler.apply(this, args);
}
}
}
}; };
@Injectable() @Injectable()
@ -86,20 +115,34 @@ export class DomEventsPlugin extends EventManagerPlugin {
let callback: EventListener = handler as EventListener; let callback: EventListener = handler as EventListener;
// if zonejs is loaded and current zone is not ngZone // if zonejs is loaded and current zone is not ngZone
// we keep Zone.current on target for later restoration. // we keep Zone.current on target for later restoration.
if (zoneJsLoaded && !NgZone.isInAngularZone()) { if (zoneJsLoaded && (!NgZone.isInAngularZone() || isBlackListedEvent(eventName))) {
let symbolName = symbolNames[eventName]; let symbolName = symbolNames[eventName];
if (!symbolName) { if (!symbolName) {
symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE); symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE);
} }
let taskDatas: TaskData[] = (element as any)[symbolName]; let taskDatas: TaskData[] = (element as any)[symbolName];
const listenerRegistered = taskDatas && taskDatas.length > 0; const globalListenerRegistered = taskDatas && taskDatas.length > 0;
if (!taskDatas) { if (!taskDatas) {
taskDatas = (element as any)[symbolName] = []; taskDatas = (element as any)[symbolName] = [];
} }
if (taskDatas.filter(taskData => taskData.handler === callback).length === 0) {
taskDatas.push({zone: Zone.current, handler: callback}); const zone = isBlackListedEvent(eventName) ? Zone.root : Zone.current;
if (taskDatas.length === 0) {
taskDatas.push({zone: zone, handler: callback});
} else {
let callbackRegistered = false;
for (let i = 0; i < taskDatas.length; i++) {
if (taskDatas[i].handler === callback) {
callbackRegistered = true;
break;
}
}
if (!callbackRegistered) {
taskDatas.push({zone: zone, handler: callback});
}
} }
if (!listenerRegistered) {
if (!globalListenerRegistered) {
element[ADD_EVENT_LISTENER](eventName, globalListener, false); element[ADD_EVENT_LISTENER](eventName, globalListener, false);
} }
} else { } else {

View File

@ -116,6 +116,132 @@ export function main() {
getDOM().dispatchEvent(element, dispatchedEvent); getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null); expect(receivedEvent).toBe(null);
}); });
it('should keep zone when addEventListener multiple times', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
getDOM().appendChild(doc.body, element);
const dispatchedEvent = getDOM().createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
const handler1 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const handler2 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover1 = null;
let remover2 = null;
Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler1); });
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler2);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name, 'test']);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should handle event correctly when one handler remove itself ', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
getDOM().appendChild(doc.body, element);
const dispatchedEvent = getDOM().createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
let remover1: any = null;
let remover2: any = null;
const handler1 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
remover1 && remover1();
};
const handler2 = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler1); });
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler2);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent, dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name, 'test']);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should only add same callback once when addEventListener', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
getDOM().appendChild(doc.body, element);
const dispatchedEvent = getDOM().createMouseEvent('click');
let receivedEvents: any[] /** TODO #9100 */ = [];
let receivedZones: any[] = [];
const handler = (e: any /** TODO #9100 */) => {
receivedEvents.push(e);
receivedZones.push(Zone.current.name);
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover1 = null;
let remover2 = null;
Zone.root.run(() => { remover1 = manager.addEventListener(element, 'click', handler); });
Zone.root.fork({name: 'test'}).run(() => {
remover2 = manager.addEventListener(element, 'click', handler);
});
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([dispatchedEvent]);
expect(receivedZones).toEqual([Zone.root.name]);
receivedEvents = [];
remover1 && remover1();
remover2 && remover2();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvents).toEqual([]);
});
it('should run blackListedEvents handler outside of ngZone', () => {
const Zone = (window as any)['Zone'];
const element = el('<div><div></div></div>');
getDOM().appendChild(doc.body, element);
const dispatchedEvent = getDOM().createMouseEvent('scroll');
let receivedEvent: any /** TODO #9100 */ = null;
let receivedZone: any = null;
const handler = (e: any /** TODO #9100 */) => {
receivedEvent = e;
receivedZone = Zone.current;
};
const manager = new EventManager([domEventPlugin], new FakeNgZone());
let remover = manager.addEventListener(element, 'scroll', handler);
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(dispatchedEvent);
expect(receivedZone.name).toBe(Zone.root.name);
receivedEvent = null;
remover && remover();
getDOM().dispatchEvent(element, dispatchedEvent);
expect(receivedEvent).toBe(null);
});
}); });
} }

9
test-events.js Normal file
View File

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