Skip to content

Commit 5d4f543

Browse files
vicbjasonaden
authored andcommitted
refactor(router): don't run the change detection every time an outlet is activated
fix(router): inside on push // SQUASH after review
1 parent 81ca51a commit 5d4f543

11 files changed

Lines changed: 349 additions & 233 deletions

File tree

‎packages/router/src/directives/router_outlet.ts‎

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Attribute, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, Output, ReflectiveInjector, ResolvedReflectiveProvider, ViewContainerRef} from '@angular/core';
10-
import {RouterOutletMap} from '../router_outlet_map';
9+
import {Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Injector, OnDestroy, OnInit, Output, ViewContainerRef} from '@angular/core';
10+
11+
import {ChildrenOutletContexts} from '../router_outlet_context';
1112
import {ActivatedRoute} from '../router_state';
1213
import {PRIMARY_OUTLET} from '../shared';
1314

@@ -36,23 +37,40 @@ import {PRIMARY_OUTLET} from '../shared';
3637
* @stable
3738
*/
3839
@Directive({selector: 'router-outlet'})
39-
export class RouterOutlet implements OnDestroy {
40+
export class RouterOutlet implements OnDestroy, OnInit {
4041
private activated: ComponentRef<any>|null = null;
4142
private _activatedRoute: ActivatedRoute|null = null;
42-
private _outletName: string;
43-
public outletMap: RouterOutletMap;
43+
private name: string;
4444

4545
@Output('activate') activateEvents = new EventEmitter<any>();
4646
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
4747

4848
constructor(
49-
private parentOutletMap: RouterOutletMap, private location: ViewContainerRef,
50-
private resolver: ComponentFactoryResolver, @Attribute('name') name: string) {
51-
this._outletName = name || PRIMARY_OUTLET;
52-
parentOutletMap.registerOutlet(this._outletName, this);
49+
private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
50+
private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
51+
private changeDetector: ChangeDetectorRef) {
52+
this.name = name || PRIMARY_OUTLET;
53+
parentContexts.onChildOutletCreated(this.name, this);
5354
}
5455

55-
ngOnDestroy(): void { this.parentOutletMap.removeOutlet(this._outletName); }
56+
ngOnDestroy(): void { this.parentContexts.onChildOutletDestroyed(this.name); }
57+
58+
ngOnInit(): void {
59+
if (!this.activated) {
60+
// If the outlet was not instantiated at the time the route got activated we need to populate
61+
// the outlet when it is initialized (ie inside a NgIf)
62+
const context = this.parentContexts.getContext(this.name);
63+
if (context && context.route) {
64+
if (context.attachRef) {
65+
// `attachRef` is populated when there is an existing component to mount
66+
this.attach(context.attachRef, context.route);
67+
} else {
68+
// otherwise the component defined in the configuration is created
69+
this.activateWith(context.route, context.resolver || null);
70+
}
71+
}
72+
}
73+
}
5674

5775
/** @deprecated since v4 **/
5876
get locationInjector(): Injector { return this.location.injector; }
@@ -102,65 +120,37 @@ export class RouterOutlet implements OnDestroy {
102120
}
103121
}
104122

105-
/** @deprecated since v4, use {@link #activateWith} */
106-
activate(
107-
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector,
108-
providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void {
109-
if (this.isActivated) {
110-
throw new Error('Cannot activate an already activated outlet');
111-
}
112-
113-
this.outletMap = outletMap;
114-
this._activatedRoute = activatedRoute;
115-
116-
const snapshot = activatedRoute._futureSnapshot;
117-
const component: any = <any>snapshot._routeConfig !.component;
118-
const factory = resolver.resolveComponentFactory(component) !;
119-
120-
const inj = ReflectiveInjector.fromResolvedProviders(providers, injector);
121-
122-
this.activated = this.location.createComponent(factory, this.location.length, inj, []);
123-
this.activated.changeDetectorRef.detectChanges();
124-
125-
this.activateEvents.emit(this.activated.instance);
126-
}
127-
128-
activateWith(
129-
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null,
130-
outletMap: RouterOutletMap) {
123+
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
131124
if (this.isActivated) {
132125
throw new Error('Cannot activate an already activated outlet');
133126
}
134-
135-
this.outletMap = outletMap;
136127
this._activatedRoute = activatedRoute;
137-
138128
const snapshot = activatedRoute._futureSnapshot;
139129
const component = <any>snapshot._routeConfig !.component;
140-
141130
resolver = resolver || this.resolver;
142131
const factory = resolver.resolveComponentFactory(component);
143-
144-
const injector = new OutletInjector(activatedRoute, outletMap, this.location.injector);
145-
132+
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
133+
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
146134
this.activated = this.location.createComponent(factory, this.location.length, injector);
147-
this.activated.changeDetectorRef.detectChanges();
148-
135+
// Calling `markForCheck` to make sure we will run the change detection when the
136+
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
137+
this.changeDetector.markForCheck();
149138
this.activateEvents.emit(this.activated.instance);
150139
}
151140
}
152141

153142
class OutletInjector implements Injector {
154143
constructor(
155-
private route: ActivatedRoute, private map: RouterOutletMap, private parent: Injector) {}
144+
private route: ActivatedRoute, private childContexts: ChildrenOutletContexts,
145+
private parent: Injector) {}
156146

157147
get(token: any, notFoundValue?: any): any {
158148
if (token === ActivatedRoute) {
159149
return this.route;
160150
}
161151

162-
if (token === RouterOutletMap) {
163-
return this.map;
152+
if (token === ChildrenOutletContexts) {
153+
return this.childContexts;
164154
}
165155

166156
return this.parent.get(token, notFoundValue);

‎packages/router/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export {DetachedRouteHandle, RouteReuseStrategy} from './route_reuse_strategy';
1717
export {NavigationExtras, Router} from './router';
1818
export {ROUTES} from './router_config_loader';
1919
export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, provideRoutes} from './router_module';
20-
export {RouterOutletMap} from './router_outlet_map';
20+
export {ChildrenOutletContexts, OutletContext} from './router_outlet_context';
2121
export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
2222
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
2323
export {PRIMARY_OUTLET, ParamMap, Params, convertToParamMap} from './shared';

‎packages/router/src/route_reuse_strategy.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {ComponentRef} from '@angular/core';
1010

11+
import {OutletContext} from './router_outlet_context';
1112
import {ActivatedRoute, ActivatedRouteSnapshot} from './router_state';
1213
import {TreeNode} from './utils/tree';
1314

@@ -23,6 +24,7 @@ export type DetachedRouteHandle = {};
2324

2425
/** @internal */
2526
export type DetachedRouteHandleInternal = {
27+
contexts: Map<string, OutletContext>,
2628
componentRef: ComponentRef<any>,
2729
route: TreeNode<ActivatedRoute>,
2830
};
@@ -36,7 +38,11 @@ export abstract class RouteReuseStrategy {
3638
/** Determines if this route (and its subtree) should be detached to be reused later */
3739
abstract shouldDetach(route: ActivatedRouteSnapshot): boolean;
3840

39-
/** Stores the detached route */
41+
/**
42+
* Stores the detached route.
43+
*
44+
* Storing a `null` value should erase the previously stored value.
45+
*/
4046
abstract store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle|null): void;
4147

4248
/** Determines if this route (and its subtree) should be reattached */

0 commit comments

Comments
 (0)