fix(platform-browser): simple version of zone aware addEventListener (#18993)

PR Close #18993
This commit is contained in:
JiaLi.Passion
2017-08-31 15:52:51 +09:00
committed by Miško Hevery
parent 35bc1eb218
commit ed1175f27e
5 changed files with 138 additions and 18 deletions

View File

@ -13,7 +13,8 @@ import {DOCUMENT} from '../dom_tokens';
import {EventManagerPlugin} from './event_manager';
/**
* Detect if Zone is present. If it is then bypass 'addEventListener' since Angular can do much more
* Detect if Zone is present. If it is then use simple zone aware 'addEventListener'
* since Angular can do much more
* efficient bookkeeping than Zone can, because we have additional information. This speeds up
* addEventListener by 3x.
*/
@ -24,6 +25,40 @@ const __symbol__ = Zone && Zone['__symbol__'] || function<T>(v: T): T {
const ADD_EVENT_LISTENER: 'addEventListener' = __symbol__('addEventListener');
const REMOVE_EVENT_LISTENER: 'removeEventListener' = __symbol__('removeEventListener');
const symbolNames: {[key: string]: string} = {};
const FALSE = 'FALSE';
const ANGULAR = 'ANGULAR';
const NATIVE_ADD_LISTENER = 'addEventListener';
const NATIVE_REMOVE_LISTENER = 'removeEventListener';
interface TaskData {
zone: any;
handler: Function;
}
// a global listener to handle all dom event,
// so we do not need to create a closure everytime
const globalListener = function(event: Event) {
const symbolName = symbolNames[event.type];
if (!symbolName) {
return;
}
const taskDatas: TaskData[] = this[symbolName];
if (!taskDatas) {
return;
}
const args: any = [event];
taskDatas.forEach(taskData => {
if (taskData.zone !== Zone.current) {
// only use Zone.run when Zone.current not equals to stored zone
return taskData.zone.run(taskData.handler, this, args);
} else {
return taskData.handler.apply(this, args);
}
});
};
@Injectable()
export class DomEventsPlugin extends EventManagerPlugin {
constructor(@Inject(DOCUMENT) doc: any, private ngZone: NgZone) { super(doc); }
@ -37,23 +72,65 @@ export class DomEventsPlugin extends EventManagerPlugin {
* This code is about to add a listener to the DOM. If Zone.js is present, than
* `addEventListener` has been patched. The patched code adds overhead in both
* memory and speed (3x slower) than native. For this reason if we detect that
* Zone.js is present we bypass zone and use native addEventListener instead.
* The result is faster registration but the zone will not be restored. We do
* manual zone restoration in element.ts renderEventHandlerClosure method.
* Zone.js is present we use a simple version of zone aware addEventListener instead.
* The result is faster registration and the zone will be restored.
* But ZoneSpec.onScheduleTask, ZoneSpec.onInvokeTask, ZoneSpec.onCancelTask
* will not be invoked
* We also do manual zone restoration in element.ts renderEventHandlerClosure method.
*
* NOTE: it is possible that the element is from different iframe, and so we
* have to check before we execute the method.
*/
const self = this;
let byPassZoneJS = element[ADD_EVENT_LISTENER];
const zoneJsLoaded = element[ADD_EVENT_LISTENER];
let callback: EventListener = handler as EventListener;
if (byPassZoneJS) {
callback = function() {
return self.ngZone.runTask(handler as any, null, arguments as any, eventName);
};
// if zonejs is loaded and current zone is not ngZone
// we keep Zone.current on target for later restoration.
if (zoneJsLoaded && !NgZone.isInAngularZone()) {
let symbolName = symbolNames[eventName];
if (!symbolName) {
symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE);
}
let taskDatas: TaskData[] = (element as any)[symbolName];
const listenerRegistered = taskDatas && taskDatas.length > 0;
if (!taskDatas) {
taskDatas = (element as any)[symbolName] = [];
}
if (taskDatas.filter(taskData => taskData.handler === callback).length === 0) {
taskDatas.push({zone: Zone.current, handler: callback});
}
if (!listenerRegistered) {
element[ADD_EVENT_LISTENER](eventName, globalListener, false);
}
} else {
element[NATIVE_ADD_LISTENER](eventName, callback, false);
}
return () => this.removeEventListener(element, eventName, callback);
}
removeEventListener(target: any, eventName: string, callback: Function): void {
let underlyingRemove = target[REMOVE_EVENT_LISTENER];
// zone.js not loaded, use native removeEventListener
if (!underlyingRemove) {
return target[NATIVE_REMOVE_LISTENER].apply(target, [eventName, callback, false]);
}
let symbolName = symbolNames[eventName];
let taskDatas: TaskData[] = symbolName && target[symbolName];
if (!taskDatas) {
// addEventListener not using patched version
// just call native removeEventListener
return target[NATIVE_REMOVE_LISTENER].apply(target, [eventName, callback, false]);
}
for (let i = 0; i < taskDatas.length; i++) {
// remove listener from taskDatas if the callback equals
if (taskDatas[i].handler === callback) {
taskDatas.splice(i, 1);
break;
}
}
if (taskDatas.length === 0) {
// all listeners are removed, we can remove the globalListener from target
underlyingRemove.apply(target, [eventName, globalListener, false]);
}
element[byPassZoneJS ? ADD_EVENT_LISTENER : 'addEventListener'](eventName, callback, false);
return () => element[byPassZoneJS ? REMOVE_EVENT_LISTENER : 'removeEventListener'](
eventName, callback as any, false);
}
}