Skip to content

Commit

Permalink
test: another test
Browse files Browse the repository at this point in the history
  • Loading branch information
JiaLiPassion committed May 18, 2019
1 parent 92692ec commit b50e1ae
Show file tree
Hide file tree
Showing 5 changed files with 29 additions and 201 deletions.
2 changes: 1 addition & 1 deletion packages/core/testing/src/ng_zone_mock.ts
Expand Up @@ -16,7 +16,7 @@ import {EventEmitter, Injectable, NgZone} from '@angular/core';
export class MockNgZone extends NgZone {
onStable: EventEmitter<any> = new EventEmitter(false);

constructor() { super({enableLongStackTrace: false}); }
constructor() { super({enableLongStackTrace: false, shouldCoalesceEventChangeDetection: false}); }

run(fn: Function): any { return fn(); }

Expand Down
6 changes: 0 additions & 6 deletions packages/platform-browser/src/dom/dom_renderer.ts
Expand Up @@ -49,12 +49,6 @@ export function flattenStyles(

function decoratePreventDefault(eventHandler: Function): Function {
return (event: any) => {
// check if Zone exists, if exists, check whether current Zone
// holds the delayChangeDetection function reference.
if (typeof Zone !== 'undefined') {
const maybeDelayChangeDetection: () => void = Zone.current.get('maybeDelayChangeDetection');
maybeDelayChangeDetection && maybeDelayChangeDetection();
}
const allowDefaultBehavior = eventHandler(event);
if (allowDefaultBehavior === false) {
// TODO(tbosch): move preventDefault into event plugins...
Expand Down
195 changes: 10 additions & 185 deletions packages/platform-browser/src/dom/events/dom_events.ts
Expand Up @@ -11,128 +11,12 @@ import {Inject, Injectable, NgZone, Optional, PLATFORM_ID} from '@angular/core';

import {EventManagerPlugin} from './event_manager';

/**
* 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.
*/
const __symbol__ =
(() => (typeof Zone !== 'undefined') && (Zone as any)['__symbol__'] ||
function(v: string): string { return '__zone_symbol__' + v; })();
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';

// use the same symbol string which is used in zone.js
const stopSymbol = '__zone_symbol__propagationStopped';
const stopMethodSymbol = '__zone_symbol__stopImmediatePropagation';


const blackListedMap = (() => {
const blackListedEvents: string[] =
(typeof Zone !== 'undefined') && (Zone as any)[__symbol__('BLACK_LISTED_EVENTS')];
if (blackListedEvents) {
const res: {[eventName: string]: string} = {};
blackListedEvents.forEach(eventName => { res[eventName] = eventName; });
return res;
}
return undefined;
})();


const isBlackListedEvent = function(eventName: string) {
if (!blackListedMap) {
return false;
}
return blackListedMap.hasOwnProperty(eventName);
};

interface TaskData {
zone: any;
handler: Function;
}

// a global listener to handle all dom event,
// so we do not need to create a closure every time
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];
if (taskDatas.length === 1) {
// if taskDatas only have one element, just invoke it
const taskData = taskDatas[0];
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);
}
} 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++) {
// if other listener call event.stopImmediatePropagation
// just break
if ((event as any)[stopSymbol] === true) {
break;
}
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()
export class DomEventsPlugin extends EventManagerPlugin {
constructor(
@Inject(DOCUMENT) doc: any, private ngZone: NgZone,
@Optional() @Inject(PLATFORM_ID) platformId: {}|null) {
super(doc);

if (!platformId || !isPlatformServer(platformId)) {
this.patchEvent();
}
}

private patchEvent() {
if (typeof Event === 'undefined' || !Event || !Event.prototype) {
return;
}
if ((Event.prototype as any)[stopMethodSymbol]) {
// already patched by zone.js
return;
}
const delegate = (Event.prototype as any)[stopMethodSymbol] =
Event.prototype.stopImmediatePropagation;
Event.prototype.stopImmediatePropagation = function() {
if (this) {
this[stopSymbol] = true;
}

// should call native delegate in case
// in some environment part of the application
// will not use the patched Event
delegate && delegate.apply(this, arguments);
};
}

// This plugin should come last in the list of plugins, because it accepts all
Expand All @@ -153,80 +37,21 @@ export class DomEventsPlugin extends EventManagerPlugin {
* 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;
const zoneJsLoaded = element[ADD_EVENT_LISTENER];
let callback: EventListener = handler as EventListener;
// if zonejs is loaded and current zone is not ngZone
// we keep Zone.current on target for later restoration.
if (zoneJsLoaded && (!NgZone.isInAngularZone() || isBlackListedEvent(eventName))) {
let symbolName = symbolNames[eventName];
if (!symbolName) {
symbolName = symbolNames[eventName] = __symbol__(ANGULAR + eventName + FALSE);
}
let taskDatas: TaskData[] = (element as any)[symbolName];
const globalListenerRegistered = taskDatas && taskDatas.length > 0;
if (!taskDatas) {
taskDatas = (element as any)[symbolName] = [];
}

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});
}
let callback: EventListener = (function(evt: Event) {
// check if Zone exists, if exists, check whether current Zone
// holds the delayChangeDetection function reference.
if (typeof Zone !== 'undefined') {
const maybeDelayChangeDetection: () => void = Zone.current.get('maybeDelayChangeDetection');
maybeDelayChangeDetection && maybeDelayChangeDetection();
}
return handler && handler.call(this, evt);
}) as EventListener;
element.addEventListener(eventName, callback);

if (!globalListenerRegistered) {
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]);
}
// fix issue 20532, should be able to remove
// listener which was added inside of ngZone
let found = false;
for (let i = 0; i < taskDatas.length; i++) {
// remove listener from taskDatas if the callback equals
if (taskDatas[i].handler === callback) {
found = true;
taskDatas.splice(i, 1);
break;
}
}
if (found) {
if (taskDatas.length === 0) {
// all listeners are removed, we can remove the globalListener from target
underlyingRemove.apply(target, [eventName, globalListener, false]);
}
} else {
// not found in taskDatas, the callback may be added inside of ngZone
// use native remove listener to remove the callback
target[NATIVE_REMOVE_LISTENER].apply(target, [eventName, callback, false]);
}
target.removeEventListener(eventName, callback);
}
}
25 changes: 17 additions & 8 deletions packages/platform-browser/test/dom/events/event_manager_spec.ts
Expand Up @@ -320,7 +320,10 @@ import {el} from '../../../testing/src/browser_util';
expect(receivedEvent).toBe(null);
});

it('should only trigger one Change detection when bubbling', () => {
it('should only trigger one Change detection when bubbling', (done: DoneFn) => {
doc = getDOM().supportsDOMEvents() ? document : getDOM().createHtmlDocument();
zone = new NgZone({shouldCoalesceEventChangeDetection: true});
domEventPlugin = new DomEventsPlugin(doc, zone, null);
const element = el('<div></div>');
const child = el('<div></div>');
getDOM().appendChild(element, child);
Expand All @@ -329,18 +332,24 @@ import {el} from '../../../testing/src/browser_util';
let receivedEvents: any = [];
let stables: any = [];
const handler = (e: any) => { receivedEvents.push(e); };
const zone = new FakeNgZone();
const manager = new EventManager([domEventPlugin], zone);
let removerChild: any;
let removerParent: any;

const removerChild = manager.addEventListener(child, 'click', handler);
const removerParent = manager.addEventListener(element, 'click', handler);
zone.run(() => {
removerChild = manager.addEventListener(child, 'click', handler);
removerParent = manager.addEventListener(element, 'click', handler);
});
zone.onStable.subscribe((isStable: any) => { stables.push(isStable); });
getDOM().dispatchEvent(child, dispatchedEvent);
expect(receivedEvents.length).toBe(2);
expect(stables.length).toBe(1);
requestAnimationFrame(() => {
expect(receivedEvents.length).toBe(2);
expect(stables.length).toBe(1);

removerChild && removerChild();
removerParent && removerParent();
removerChild && removerChild();
removerParent && removerParent();
done();
});
});
});
})();
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-browser/testing/src/browser_util.ts
Expand Up @@ -157,5 +157,5 @@ export function stringifyElement(el: any /** TODO #9100 */): string {
}

export function createNgZone(): NgZone {
return new NgZone({enableLongStackTrace: true});
return new NgZone({enableLongStackTrace: true, shouldCoalesceEventChangeDetection: false});
}

0 comments on commit b50e1ae

Please sign in to comment.