Why do my angular2 components get re-instantiated

2019-02-21 12:02发布

问题:

I am trying to put together a demo app of Angular 2 + Rx.JS 5/Next.

I noticed that my components are re-instantiated each time I switch the route.

Here is the code for the root app:

import {bootstrap}    from 'angular2/platform/browser';
import {HTTP_PROVIDERS} from 'angular2/http';
import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component.ts';

bootstrap(AppComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS]);

Here is the code for the root component:

import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {FirstComponent} from './app.first-component.ts';
import {SecondComponent} from './app.second-component.ts';
import {AppService} from "./app.services.ts";


@Component({
    selector: 'my-app',
    providers: [AppService, FirstComponent, SecondComponent],
    directives: [FirstComponent, SecondComponent, ROUTER_DIRECTIVES],
    template: `<h1>An Angular 2 App</h1>
               <a [routerLink]="['First']">first-default</a> 
               <a [routerLink]="['Second']">second</a> 
               <router-outlet></router-outlet>`
})
@RouteConfig([
    {path: '/', name: 'First', component: FirstComponent, useAsDefault: true},
    {path: '/second', name: 'Second', component: SecondComponent}
])
export class AppComponent {
}

Then the code for the first component (mapped to /):

import {Component, OnInit, NgZone} from "angular2/core";
import {AppService} from "./app.services.ts";
import 'rxjs/Rx';


@Component({
    selector: 'my-first',
    template: `
<div>
    <ul>
        <li *ngFor="#s of someStrings">
           a string: {{ s }}
        </li>
    </ul>
 </div>`
})
export class FirstComponent implements OnInit {

    zone:NgZone;

    constructor(private appService:AppService) {
        console.log('constructor', 'first');
        this.zone = new NgZone({enableLongStackTrace: false});
    }

    someStrings:string[] = [];

    ngOnInit() {
        console.log('ngOnInit', 'first');
        this.appService.refCounted.subscribe(
            theStrings=> {
                this.zone.run(() =>this.someStrings.push(...theStrings));
            },
            error=>console.log(error)
        );
    }
}

And the second component (mapped to /second):

import {Component, OnInit, NgZone} from "angular2/core";
import {AppService} from "./app.services.ts";

@Component({
    selector: 'my-second',
    template: `
<div>
    <ul>
        <li *ngFor="#s of someStrings">
           a string: {{ s }}
        </li>
    </ul>
 </div>`
})
export class SecondComponent implements OnInit {

    zone:NgZone;

    constructor(private appService:AppService) {
        console.log('constructor', 'second');
        this.zone = new NgZone({enableLongStackTrace: false});
    }

    someStrings:string[] = [];

    ngOnInit() {
        console.log('ngOnInit', 'second');
        this.appService.refCounted.subscribe(
            theStrings=> {
                this.zone.run(() =>this.someStrings.push(...theStrings));
            },
            error=>console.log(error)
        );
    }
}

And finally the app service (slightly less relevant to this question):

import {Injectable} from "angular2/core";
import {Observable} from "rxjs/Observable";
import {Subject} from "rxjs/Subject";
import 'rxjs/Rx';


@Injectable()
export class AppService {

    constructor(){
        console.log('constructor', 'appService');
    }

    someObservable$:Observable<string[]> = Observable.create(observer => {
        const eventSource = new EventSource('/interval-sse-observable');
        eventSource.onmessage = x => observer.next(JSON.parse(x.data));
        eventSource.onerror = x => observer.error(console.log('EventSource failed'));

        return () => {
            eventSource.close();
        };
    });

    subject$ = new Subject();

    refCounted = this.someObservable$.multicast(this.subject$).refCount();

    someMethod_() {
        let someObservable$:Observable<string[]> = Observable.create(observer => {
            const eventSource = new EventSource('/interval-sse-observable');
            eventSource.onmessage = x => observer.next(JSON.parse(x.data));
            eventSource.onerror = x => observer.error(console.log('EventSource failed'));

            return () => {
                eventSource.close();
            };
        });
        return someObservable$;
    }
}

So in order to debug the instantiation of the First and Second components, I have added a console.log in the constructors/ngOnInit:

and I noticed that each time I change the route by clicking on the links, I get:

constructor first
ngOnInit first

constructor second
ngOnInit second
...

Can someone please advise whether this is expected behavior? If so how can I get Angular2 to instantiate my components only once?

Note that I have explicitly required that the First and Second components be instantiated at the root-level component by adding a providers array there.

P.S. Here is the github repository for this project: https://github.com/balteo/demo-angular2-rxjs/tree/WITH-ROUTER

edit:

I am still trying to find a solution to this problem. I am getting something wrong with the router or my components. I have pushed the app to github here hoping someone can provide advice.

回答1:

>= 2.3.0-rc.0

A custom RouteReuseStrategy can be implemented to control when routed components are destroyed and recreated or reused.

  • https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html
  • https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx
  • https://github.com/angular/angular/issues/7757

>= 2.0.0

CanReuse doesn't exist anymore in the new router. When only route parameters change while staying on the same route, the component is not recreated.

If the route is changed and navigated back to the same component the component is recreated.

<=RC.x

Note that I have explicitly required that the First and Second components be instantiated at the root-level component by adding a providers array there.

Adding components to providers: [] is mostly meaningless especially in regard to this issue.

You can implement CanReuse

by adding

routerCanReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; }

to your component, but it's quite limited for which kind of navigation the same instance is reused (if you stay on the same route and only change parameters).

If CanReuse doesn't fix your problem then move the data to a service, where instances are kept and bind the view of the component to the data of this service to make the component show current data.



回答2:

It is absolutely expected behavior that the components will be constructed and destructed as your route changes.

Remember, components are closely associated with the DOM elements of your templates. As these dom elements are removed or changed, so must the component be destructed and a new one constructed.

There are a few exceptions to this, but not as it relates to your use-case (for example the CanReuse method mentioned in another answer, but that's meant for something completely different.

What CanReuse does, is when going from a route (let's call it route1) to another route (route2), and both routes use the same component MyComponent, if you tell it that it's allowed to re-use the component, it is basically saying:

"Since the route I'm going too uses the exact same component type as the route I'm currently on, it's ok to reuse my current component instance on the new route" (perhaps because it's stateless for example).

You don't say exactly what you are trying to achieve, or why it is important for your two components to be instantiated only once, but in general, components are not intended to store long-term application state (or more precisely, state that outlives the lifetime of the dom element associated with the component). These sort of things should live elsewhere (e.g. in services) and shared between components through injection (or passed in as Inputs).



回答3:

"It is absolutely expected behavior that the components will be constructed and destructed as your route changes"

The point is that components should NOT be reconstructed again and again as one navigates back and forth between two links (that are different from each other). What if you have complex graphics and state that you are modifying on one link and the object is destroyed and recreated as you arrive back to resume your work?

A framework should not dictate if the objects should be destroyed and recreated again and again. It should provide both options and in the default option, it should NOT destroy and recreate as this is the intuitive behavior. This is the behavior most UI frameworks have used over the years.