feat: add a temp solution to support passive event listeners. (#34503)

Now Angular doesn't support add event listeners as passive very easily.
User needs to use `elem.addEventListener('scroll', listener, {passive: true});`
or implements their own EventManagerPlugin to do that.
Angular may finally support new template syntax to support passive event, for now,
this commit introduces a temp solution to allow user to define the passive event names
in zone.js configurations.

User can define a global varibale like this.

```
(window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll'];
```

to let all `scroll` event listeners passive.

PR Close #34503
This commit is contained in:
JiaLiPassion 2020-02-22 10:56:25 +09:00 committed by Miško Hevery
parent af76651ccc
commit f9d483e76e
4 changed files with 93 additions and 38 deletions

View File

@ -298,7 +298,23 @@ Following is all the code discussed in this page.
</code-tabs> </code-tabs>
Angular also supports passive event listeners. For example, you can use the following steps to make the scroll event passive.
1. Create a file `zone-flags.ts` under `src` directory.
2. Add the following line into this file.
```
(window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll'];
```
3. In the `src/polyfills.ts` file, before importing zone.js, import the newly created `zone-flags`.
```
import './zone-flags';
import 'zone.js/dist/zone'; // Included with Angular CLI.
```
After those steps, if you add event listeners for the `scroll` event, the listeners will be `passive`.
## Summary ## Summary

View File

@ -25,7 +25,6 @@ if (typeof window !== 'undefined') {
try { try {
const options = const options =
Object.defineProperty({}, 'passive', {get: function() { passiveSupported = true; }}); Object.defineProperty({}, 'passive', {get: function() { passiveSupported = true; }});
window.addEventListener('test', options, options); window.addEventListener('test', options, options);
window.removeEventListener('test', options, options); window.removeEventListener('test', options, options);
} catch (err) { } catch (err) {
@ -245,16 +244,30 @@ export function patchEventTarget(
proto[patchOptions.prepend]; proto[patchOptions.prepend];
} }
function checkIsPassive(task: Task) { /**
if (!passiveSupported && typeof taskData.options !== 'boolean' && * This util function will build an option object with passive option
typeof taskData.options !== 'undefined' && taskData.options !== null) { * to handle all possible input from the user.
// options is a non-null non-undefined object */
// passive is not supported function buildEventListenerOptions(options: any, passive: boolean) {
// don't pass options as object if (!passiveSupported && typeof options === 'object' && options) {
// just pass capture as a boolean // doesn't support passive but user want to pass an object as options.
(task as any).options = !!taskData.options.capture; // this will not work on some old browser, so we just pass a boolean
taskData.options = (task as any).options; // as useCapture parameter
return !!options.capture;
} }
if (!passiveSupported || !passive) {
return options;
}
if (typeof options === 'boolean') {
return {capture: options, passive: true};
}
if (!options) {
return {passive: true};
}
if (typeof options === 'object' && options.passive !== false) {
return {...options, passive: true};
}
return options;
} }
const customScheduleGlobal = function(task: Task) { const customScheduleGlobal = function(task: Task) {
@ -263,7 +276,6 @@ export function patchEventTarget(
if (taskData.isExisting) { if (taskData.isExisting) {
return; return;
} }
checkIsPassive(task);
return nativeAddEventListener.call( return nativeAddEventListener.call(
taskData.target, taskData.eventName, taskData.target, taskData.eventName,
taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback, taskData.capture ? globalZoneAwareCaptureCallback : globalZoneAwareCallback,
@ -311,7 +323,6 @@ export function patchEventTarget(
}; };
const customScheduleNonGlobal = function(task: Task) { const customScheduleNonGlobal = function(task: Task) {
checkIsPassive(task);
return nativeAddEventListener.call( return nativeAddEventListener.call(
taskData.target, taskData.eventName, task.invoke, taskData.options); taskData.target, taskData.eventName, task.invoke, taskData.options);
}; };
@ -338,6 +349,7 @@ export function patchEventTarget(
(patchOptions && patchOptions.diff) ? patchOptions.diff : compareTaskCallbackVsDelegate; (patchOptions && patchOptions.diff) ? patchOptions.diff : compareTaskCallbackVsDelegate;
const blackListedEvents: string[] = (Zone as any)[zoneSymbol('BLACK_LISTED_EVENTS')]; const blackListedEvents: string[] = (Zone as any)[zoneSymbol('BLACK_LISTED_EVENTS')];
const passiveEvents: string[] = _global[zoneSymbol('PASSIVE_EVENTS')];
const makeAddListener = function( const makeAddListener = function(
nativeListener: any, addSource: string, customScheduleFn: any, customCancelFn: any, nativeListener: any, addSource: string, customScheduleFn: any, customCancelFn: any,
@ -372,29 +384,25 @@ export function patchEventTarget(
return; return;
} }
const options = arguments[2]; const passive =
passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1;
const options = buildEventListenerOptions(arguments[2], passive);
if (blackListedEvents) { if (blackListedEvents) {
// check black list // check black list
for (let i = 0; i < blackListedEvents.length; i++) { for (let i = 0; i < blackListedEvents.length; i++) {
if (eventName === blackListedEvents[i]) { if (eventName === blackListedEvents[i]) {
return nativeListener.apply(this, arguments); if (passive) {
return nativeListener.call(target, eventName, delegate, options);
} else {
return nativeListener.apply(this, arguments);
}
} }
} }
} }
let capture; const capture = !options ? false : typeof options === 'boolean' ? true : options.capture;
let once = false; const once = options && typeof options === 'object' ? options.once : false;
if (options === undefined) {
capture = false;
} else if (options === true) {
capture = true;
} else if (options === false) {
capture = false;
} else {
capture = options ? !!options.capture : false;
once = options ? !!options.once : false;
}
const zone = Zone.current; const zone = Zone.current;
let symbolEventNames = zoneSymbolEventNames[eventName]; let symbolEventNames = zoneSymbolEventNames[eventName];
@ -508,17 +516,7 @@ export function patchEventTarget(
} }
const options = arguments[2]; const options = arguments[2];
let capture; const capture = !options ? false : typeof options === 'boolean' ? true : options.capture;
if (options === undefined) {
capture = false;
} else if (options === true) {
capture = true;
} else if (options === false) {
capture = false;
} else {
capture = options ? !!options.capture : false;
}
const delegate = arguments[1]; const delegate = arguments[1];
if (!delegate) { if (!delegate) {
return nativeRemoveEventListener.apply(this, arguments); return nativeRemoveEventListener.apply(this, arguments);

View File

@ -222,6 +222,7 @@ describe('Zone', function() {
}); });
zone.run(() => { document.dispatchEvent(scrollEvent); }); zone.run(() => { document.dispatchEvent(scrollEvent); });
(document as any).removeAllListeners('scroll');
}); });
it('should be able to clear on handler added before load zone.js', function() { it('should be able to clear on handler added before load zone.js', function() {
@ -799,6 +800,7 @@ describe('Zone', function() {
button.dispatchEvent(clickEvent); button.dispatchEvent(clickEvent);
expect(logs).toEqual([]); expect(logs).toEqual([]);
(document as any).removeAllListeners('click');
}); });
})); }));
@ -1035,6 +1037,42 @@ describe('Zone', function() {
button.removeEventListener('click', listener); button.removeEventListener('click', listener);
})); }));
describe('passiveEvents by global settings', () => {
let logs: string[] = [];
const listener = (e: Event) => {
logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run');
e.preventDefault();
logs.push(e.defaultPrevented ? 'defaultPrevented' : 'default will run');
};
const testPassive = function(eventName: string, expectedPassiveLog: string, options: any) {
(button as any).addEventListener(eventName, listener, options);
const evt = document.createEvent('Event');
evt.initEvent(eventName, true, true);
button.dispatchEvent(evt);
expect(logs).toEqual(['default will run', expectedPassiveLog]);
(button as any).removeAllListeners(eventName);
};
beforeEach(() => { logs = []; });
it('should be passive with global variable defined',
() => { testPassive('touchstart', 'default will run', {passive: true}); });
it('should not be passive without global variable defined',
() => { testPassive('touchend', 'defaultPrevented', undefined); });
it('should be passive with global variable defined even without passive options',
() => { testPassive('touchstart', 'default will run', undefined); });
it('should be passive with global variable defined even without passive options and with capture',
() => { testPassive('touchstart', 'default will run', {capture: true}); });
it('should be passive with global variable defined with capture option',
() => { testPassive('touchstart', 'default will run', true); });
it('should not be passive with global variable defined with passive false option',
() => { testPassive('touchstart', 'defaultPrevented', {passive: false}); });
it('should be passive with global variable defined and also blacklisted', () => {
(document as any).removeAllListeners('scroll');
testPassive('scroll', 'default will run', undefined);
});
it('should not be passive without global variable defined and also blacklisted',
() => { testPassive('wheel', 'defaultPrevented', undefined); });
});
it('should support Event.stopImmediatePropagation', it('should support Event.stopImmediatePropagation',
ifEnvSupports(supportEventListenerOptions, function() { ifEnvSupports(supportEventListenerOptions, function() {
const hookSpy = jasmine.createSpy('hook'); const hookSpy = jasmine.createSpy('hook');

View File

@ -78,5 +78,8 @@
global['__Zone_ignore_on_properties'] = global['__Zone_ignore_on_properties'] =
[{target: TestTarget.prototype, ignoreProperties: ['prop1']}]; [{target: TestTarget.prototype, ignoreProperties: ['prop1']}];
global[zoneSymbolPrefix + 'FakeAsyncTestMacroTask'] = [{source: 'TestClass.myTimeout'}]; global[zoneSymbolPrefix + 'FakeAsyncTestMacroTask'] = [{source: 'TestClass.myTimeout'}];
global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll']; // will not monkey patch scroll and wheel event.
global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll', 'wheel'];
// touchstart and scroll will be passive by default.
global[zoneSymbolPrefix + 'PASSIVE_EVENTS'] = ['touchstart', 'scroll'];
})(typeof window === 'object' && window || typeof self === 'object' && self || global); })(typeof window === 'object' && window || typeof self === 'object' && self || global);