From 45a4c6b25ba1414df7bac3ce2475001b558ffe85 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Thu, 11 Apr 2019 22:28:57 -0400 Subject: [PATCH] Enable global event dispatcher listeners to be lazily created. Prior to this change, all events that would ever possibly be used would be eagerly setup by the event dispatcher's `setup` method. Unfortunately, this has (at least) two major downsides: * As of Chrome 51, adding listeners for touch events (`touchstart`, `touchmove`, `touchend`, etc) without the `passive` flag issue a performance focused warning. See https://www.chromestatus.com/feature/5745543795965952 for more details. * A number of the events that we have historically listened for fire **massive** numbers of events (`mouseenter`, `mousemove`, etc) and most applications do not really use these events. The two primary entry points into using the event dispatcher are the `action` element modifier and `Ember.Component`'s implementing the "event delegation" methods. This commit enables the event listeners in the event dispatcher to be lazily setup on demand by updating the action modifier manager and the curly component manager to invoke the event dispatchers `setupHandler` method lazily when needed. --- .../glimmer/lib/component-managers/curly.ts | 15 +++++++++++ .../-internals/glimmer/lib/component.ts | 2 +- .../-internals/glimmer/lib/environment.ts | 5 ++++ .../glimmer/lib/modifiers/action.ts | 26 ++++++++++++++++++- .../@ember/-internals/glimmer/lib/resolver.ts | 12 ++++----- .../glimmer/lib/template-compiler.ts | 7 +++-- .../views/lib/system/event_dispatcher.js | 26 ++++++++++++++----- 7 files changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index 37177b06b2b..b5b56e55291 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -105,6 +105,19 @@ const EMPTY_POSITIONAL_ARGS: VersionedPathReference[] = []; debugFreeze(EMPTY_POSITIONAL_ARGS); +function _setupLazyEventsForComponent(dispatcher: any, component: object) { + // non-interactive rendering (e.g. SSR) has no event dispatcher + if (dispatcher === undefined) { return; } + + let lazyEvents = dispatcher._lazyEvents; + + lazyEvents.forEach((mappedEventName: string, event: string) => { + if (mappedEventName !== null && typeof component[mappedEventName] === 'function') { + dispatcher.setupHandler(event, mappedEventName); + } + }); +} + export default class CurlyComponentManager extends AbstractManager implements @@ -311,6 +324,8 @@ export default class CurlyComponentManager } } + _setupLazyEventsForComponent(environment.eventDispatcher, component); + // Track additional lifecycle metadata about this component in a state bucket. // Essentially we're saving off all the state we'll need in the future. let bucket = new ComponentStateBucket( diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index d51999a9d39..3d43217a309 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -735,7 +735,7 @@ const Component = CoreView.extend( !this.renderer._destinedForDOM || !(() => { let eventDispatcher = getOwner(this).lookup('event_dispatcher:main'); - let events = (eventDispatcher && eventDispatcher._finalEvents) || {}; + let events = (eventDispatcher && eventDispatcher._finalEventNameMapping) || {}; // tslint:disable-next-line:forin for (let key in events) { diff --git a/packages/@ember/-internals/glimmer/lib/environment.ts b/packages/@ember/-internals/glimmer/lib/environment.ts index fda6bcae98b..13f5bbc2c0f 100644 --- a/packages/@ember/-internals/glimmer/lib/environment.ts +++ b/packages/@ember/-internals/glimmer/lib/environment.ts @@ -35,6 +35,7 @@ export default class Environment extends GlimmerEnvironment { public debugStack: typeof DebugStack; public inTransaction = false; + public eventDispatcher: any; constructor(injections: any) { super(injections); @@ -49,6 +50,10 @@ export default class Environment extends GlimmerEnvironment { if (DEBUG) { this.debugStack = new DebugStack(); } + + if (this.isInteractive) { + this.eventDispatcher = this.owner.lookup('event_dispatcher:main'); + } } // this gets clobbered by installPlatformSpecificProtocolForURL diff --git a/packages/@ember/-internals/glimmer/lib/modifiers/action.ts b/packages/@ember/-internals/glimmer/lib/modifiers/action.ts index 2cab05c658a..68622eb11dd 100644 --- a/packages/@ember/-internals/glimmer/lib/modifiers/action.ts +++ b/packages/@ember/-internals/glimmer/lib/modifiers/action.ts @@ -14,6 +14,7 @@ import { } from '@glimmer/runtime'; import { Destroyable } from '@glimmer/util'; import { INVOKE } from '../utils/references'; +import { Owner } from '@ember/-internals/owner'; const MODIFIERS = ['alt', 'shift', 'meta', 'ctrl']; const POINTER_EVENT_TYPE_REGEX = /^click|mouse|touch/; @@ -187,6 +188,23 @@ export class ActionState { // implements ModifierManager export default class ActionModifierManager implements ModifierManager { + public owner: Owner; + private _setupEventHandler?: (eventName: string) => void; + + + constructor(owner: Owner) { + this.owner = owner; + } + + get setupEventHandler(): (eventName: string) => void { + if (this._setupEventHandler === undefined) { + let dispatcher = this.owner.lookup('event_dispatcher:main'); + this._setupEventHandler = (eventName) => dispatcher['setupHandler'](eventName); + } + + return this._setupEventHandler; + } + create( element: Simple.Element, _state: Opaque, @@ -246,6 +264,8 @@ export default class ActionModifierManager implements ModifierManager { public compiler: LazyCompiler; @@ -102,7 +98,7 @@ export default class RuntimeResolver implements IRuntimeResolver> = new Map(); @@ -114,10 +110,14 @@ export default class RuntimeResolver implements IRuntimeResolver(new CompileTimeLookup(this), this, macros); + + this.builtInModifiers = { + action: { manager: new ActionModifierManager(owner), state: null }, + }; } /*** IRuntimeResolver ***/ diff --git a/packages/@ember/-internals/glimmer/lib/template-compiler.ts b/packages/@ember/-internals/glimmer/lib/template-compiler.ts index 1e67214d7b5..5a88e686d6d 100644 --- a/packages/@ember/-internals/glimmer/lib/template-compiler.ts +++ b/packages/@ember/-internals/glimmer/lib/template-compiler.ts @@ -1,9 +1,12 @@ import { Compiler } from '@glimmer/interfaces'; import RuntimeResolver from './resolver'; +import { getOwner } from '@ember/-internals/owner'; // factory for DI export default { - create(): Compiler { - return new RuntimeResolver().compiler; + create(props: any): Compiler { + let owner = getOwner(props); + + return new RuntimeResolver(owner).compiler; }, }; diff --git a/packages/@ember/-internals/views/lib/system/event_dispatcher.js b/packages/@ember/-internals/views/lib/system/event_dispatcher.js index 3cb5295b3b4..032f306e31d 100644 --- a/packages/@ember/-internals/views/lib/system/event_dispatcher.js +++ b/packages/@ember/-internals/views/lib/system/event_dispatcher.js @@ -127,6 +127,9 @@ export default EmberObject.extend({ ); this._eventHandlers = Object.create(null); + this._finalEventNameMapping = null; + this._sanitizedRootElement = null; + this._lazyEvents = new Map(); }, /** @@ -142,7 +145,8 @@ export default EmberObject.extend({ @param addedEvents {Object} */ setup(addedEvents, _rootElement) { - let events = (this._finalEvents = assign({}, get(this, 'events'), addedEvents)); + let events = (this._finalEventNameMapping = assign({}, get(this, 'events'), addedEvents)); + let lazyEvents = this._lazyEvents; if (_rootElement !== undefined && _rootElement !== null) { set(this, 'rootElement', _rootElement); @@ -216,9 +220,13 @@ export default EmberObject.extend({ } } + // save off the final sanitized root element (for usage in setupHandler) + this._sanitizedRootElement = rootElement; + + // setup event listeners for the non-lazily setup events for (let event in events) { if (events.hasOwnProperty(event)) { - this.setupHandler(rootElement, event, events[event]); + lazyEvents.set(event, events[event]); } } }, @@ -234,12 +242,16 @@ export default EmberObject.extend({ @private @method setupHandler @param {Element} rootElement - @param {String} event the browser-originated event to listen to + @param {String} event the name of the event in the browser @param {String} eventName the name of the method to call on the view */ - setupHandler(rootElement, event, eventName) { - if (eventName === null) { - return; + setupHandler( + event, + eventName = this._finalEventNameMapping[event], + rootElement = this._sanitizedRootElement + ) { + if (eventName === null || !this._lazyEvents.has(event)) { + return; // nothing to do } if (!JQUERY_INTEGRATION || jQueryDisabled) { @@ -417,6 +429,8 @@ export default EmberObject.extend({ } }); } + + this._lazyEvents.delete(event); }, destroy() {