From d7d359e3eea93ab4165e69a3622cf4c7f448b56f Mon Sep 17 00:00:00 2001 From: JiaLiPassion Date: Fri, 20 Dec 2019 07:50:09 +0900 Subject: [PATCH] feat: support passive event options by defining global variables in zone.js config file (#34503) PR Close #34503 --- aio/content/guide/user-input.md | 17 +++++++++ integration/_payload-limits.json | 12 +++--- packages/zone.js/lib/common/events.ts | 36 +++++++++++++++++- packages/zone.js/test/browser/browser.spec.ts | 38 +++++++++++++++++++ packages/zone.js/test/test_fake_polyfill.ts | 3 +- 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/aio/content/guide/user-input.md b/aio/content/guide/user-input.md index c7b275afac..0b7e1b23dc 100644 --- a/aio/content/guide/user-input.md +++ b/aio/content/guide/user-input.md @@ -297,7 +297,24 @@ Following is all the code discussed in this page. +### Passive events +Angular also support passive event listeners. For example, we can use the following steps to make scroll event passive. + +1. create a file `src/zone-flags.ts`. +2. add the following line into this file. + +``` +(window as any)['__zone_symbol__PASSIVE_EVENTS'] = ['scroll']; +``` +3. in `src/polyfills.ts`, before import zone.js, import the new created `zone-flags`. + +``` +import './zone-flags'; +import 'zone.js/dist/zone'; // Included with Angular CLI. +``` + +after those steps, if user add event listeners for the `passive` event, the listeners will be `passive`. ## Summary diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 8441e070c9..2560593ecc 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -4,7 +4,7 @@ "uncompressed": { "runtime-es2015": 1485, "main-es2015": 141569, - "polyfills-es2015": 36657 + "polyfills-es2015": 37300 } } }, @@ -13,7 +13,7 @@ "uncompressed": { "runtime-es2015": 1485, "main-es2015": 16514, - "polyfills-es2015": 36657 + "polyfills-es2015": 37118 } } }, @@ -22,7 +22,7 @@ "uncompressed": { "runtime-es2015": 1485, "main-es2015": 147647, - "polyfills-es2015": 36657 + "polyfills-es2015": 37300 } } }, @@ -31,7 +31,7 @@ "uncompressed": { "runtime-es2015": 1485, "main-es2015": 136777, - "polyfills-es2015": 37334 + "polyfills-es2015": 37980 } } }, @@ -40,7 +40,7 @@ "uncompressed": { "runtime-es2015": 2289, "main-es2015": 247198, - "polyfills-es2015": 36657, + "polyfills-es2015": 36958, "5-es2015": 751 } } @@ -50,7 +50,7 @@ "uncompressed": { "runtime-es2015": 2289, "main-es2015": 226144, - "polyfills-es2015": 36657, + "polyfills-es2015": 37300, "5-es2015": 779 } } diff --git a/packages/zone.js/lib/common/events.ts b/packages/zone.js/lib/common/events.ts index 85818b4d12..0505c6ec5e 100644 --- a/packages/zone.js/lib/common/events.ts +++ b/packages/zone.js/lib/common/events.ts @@ -257,6 +257,30 @@ export function patchEventTarget( } } + /** + * + * this util function will build an option object with passive option + * to handle all possible input from the user + */ + function buildPassiveOptions(options: any, passive: boolean) { + if (!passiveSupported || !passive) { + return options; + } + if (!options) { + return {passive: true}; + } + if (typeof options === 'boolean') { + return {capture: options, passive: true}; + } + if (typeof options === 'object') { + if (typeof options.passive === 'boolean' && !options.passive) { + return options; + } + return {...options, passive: true}; + } + return options; + } + const customScheduleGlobal = function(task: Task) { // if there is already a task for the eventName + capture, // just return, because we use the shared globalZoneAwareCallback here. @@ -338,6 +362,7 @@ export function patchEventTarget( (patchOptions && patchOptions.diff) ? patchOptions.diff : compareTaskCallbackVsDelegate; const blackListedEvents: string[] = (Zone as any)[zoneSymbol('BLACK_LISTED_EVENTS')]; + const passiveEvents: string[] = _global[zoneSymbol('PASSIVE_EVENTS')]; const makeAddListener = function( nativeListener: any, addSource: string, customScheduleFn: any, customCancelFn: any, @@ -373,12 +398,19 @@ export function patchEventTarget( } const options = arguments[2]; + const passive = + passiveSupported && !!passiveEvents && passiveEvents.indexOf(eventName) !== -1; if (blackListedEvents) { // check black list for (let i = 0; i < blackListedEvents.length; i++) { if (eventName === blackListedEvents[i]) { - return nativeListener.apply(this, arguments); + if (passive) { + const passiveOptions = buildPassiveOptions(options, passive); + return nativeListener.call(target, eventName, delegate, passiveOptions); + } else { + return nativeListener.apply(this, arguments); + } } } } @@ -431,7 +463,7 @@ export function patchEventTarget( } // do not create a new object as task.data to pass those things // just use the global shared one - taskData.options = options; + taskData.options = buildPassiveOptions(options, passive); if (once) { // if addEventListener with once options, we don't pass it to // native addEventListener, instead we keep the once setting diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts index 71263adbbd..3f3f2d5965 100644 --- a/packages/zone.js/test/browser/browser.spec.ts +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -222,6 +222,7 @@ describe('Zone', function() { }); zone.run(() => { document.dispatchEvent(scrollEvent); }); + (document as any).removeAllListeners('scroll'); }); it('should be able to clear on handler added before load zone.js', function() { @@ -799,6 +800,7 @@ describe('Zone', function() { button.dispatchEvent(clickEvent); expect(logs).toEqual([]); + (document as any).removeAllListeners('click'); }); })); @@ -1035,6 +1037,42 @@ describe('Zone', function() { button.removeEventListener('click', listener); })); + describe('passiveEvents', () => { + 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', ifEnvSupports(supportEventListenerOptions, function() { const hookSpy = jasmine.createSpy('hook'); diff --git a/packages/zone.js/test/test_fake_polyfill.ts b/packages/zone.js/test/test_fake_polyfill.ts index a0787692bd..b0ccd22787 100644 --- a/packages/zone.js/test/test_fake_polyfill.ts +++ b/packages/zone.js/test/test_fake_polyfill.ts @@ -78,5 +78,6 @@ global['__Zone_ignore_on_properties'] = [{target: TestTarget.prototype, ignoreProperties: ['prop1']}]; global[zoneSymbolPrefix + 'FakeAsyncTestMacroTask'] = [{source: 'TestClass.myTimeout'}]; - global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll']; + global[zoneSymbolPrefix + 'UNPATCHED_EVENTS'] = ['scroll', 'wheel']; + global[zoneSymbolPrefix + 'PASSIVE_EVENTS'] = ['touchstart', 'scroll']; })(typeof window === 'object' && window || typeof self === 'object' && self || global);