为什么每次我改变路由时,我的angular2组件都会被重新实例化?

14

我正在尝试制作一个使用Angular 2 + Rx.JS 5/Next的演示应用程序。

我注意到每次切换路由时,我的组件都会重新实例化。

这是根应用程序的代码:

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]);

这里是根组件的代码:

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 {
}

然后是第一个组件的代码(映射到/):

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)
        );
    }
}

第二个组件(映射到/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)
        );
    }
}

最后是应用程序服务(与此问题稍微不相关):

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$;
    }
}

因此,为了调试FirstSecond组件的实例化,我已经在构造函数/ngOnInit中添加了console.log:

我注意到每次通过点击链接改变路由时,我都会得到:

constructor first
ngOnInit first

constructor second
ngOnInit second
...

请问这种行为是否符合预期?如果是,如何让Angular2只实例化我的组件一次?

请注意,我已经明确要求在根级组件中添加一个providers数组,以便显式要求实例化FirstSecond组件。

P.S. 这个项目的github存储库在这里:https://github.com/balteo/demo-angular2-rxjs/tree/WITH-ROUTER

编辑:

我仍在努力找到解决这个问题的方法。 我在这里将应用程序推送到github,希望有人能提供建议。


你解决了吗? - pd farhad
3个回答

8

>= 2.3.0-rc.0

可以实现自定义的RouteReuseStrategy来控制路由组件何时被销毁、重建或重用。

>= 2.0.0

CanReuse在新的路由器中已经不存在了。当只有路由参数改变时,组件不会被重新创建,但仍然停留在同一路由上。

如果路由被更改并返回到相同的组件,则该组件将被重新创建。

<=RC.x

请注意,我已明确要求在根级组件中添加提供程序数组以实例化第一个和第二个组件。

将组件添加到providers: []中大多是无意义的,特别是在涉及此问题时。

您可以通过添加

CanReuse来实现。
routerCanReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; }

将数据绑定到您的组件中,但它对于重新使用相同实例的导航类型相当有限(如果您停留在相同的路由上并仅更改参数)。

如果CanReuse无法解决您的问题,则将数据移动到服务中,在此处保留实例,并将组件视图绑定到此服务的数据以显示当前数据。


1
非常感谢您,Günter。我正在尝试实现第二种解决方案(将数据移入服务)。当我成功实现它时,我会接受这个答案。 - balteo
我已经打开了另一个与原始帖子相关的帖子。请参见http://stackoverflow.com/questions/36866215 - balteo
我已经更新了 Plunker。这是链接:https://plnkr.co/edit/y3MBjvitr6HnR2ymTRe8 - balteo
可以在控制台中看到,每次单击链接时组件都会被重新实例化... - balteo
routerCanReuse()在这种情况下不起作用,也不是它的预期用途。在这里,我们在两个不同的组件类型之间切换,这将始终导致组件被重新实例化。'routerCanReuse()'是一种优化,当您有使用相同组件类型的不同路由(可能具有不同的路由参数)时,可以进行重用。在这种情况下,重用意味着为该其他路由重用相同的实例。 - Daniel Tabuenca
显示剩余5条评论

3
组件在路由变化时被构建和销毁是完全预期的行为。请记住,组件与模板的DOM元素密切相关。随着这些DOM元素的移除或更改,组件也必须被销毁并重新构建一个新的组件。虽然有一些例外情况,但与您的用例无关(例如另一个答案中提到的CanReuse方法,但那是针对完全不同的东西)。CanReuse的作用是,当从一个路由(假设称为route1)转到另一个路由(route2),并且两个路由都使用相同的组件MyComponent时,如果告诉它允许重用该组件,则基本上是说:“由于我要去的路由使用与我当前所在路由完全相同的组件类型,因此可以在新路由上重用我的当前组件实例”(可能是因为它是无状态的)。你没有明确说明你想要实现什么,或者为什么只有两个组件被实例化一次很重要,但通常情况下,组件不打算存储长期应用程序状态(或更准确地说,状态超出与组件关联的DOM元素的生命周期)。这些东西应该存在其他地方(例如服务)并通过注入(或作为输入传递)在组件之间共享。

1
"预期的行为是当你的路由改变时,组件将被构建和销毁。"
"重点在于,当我们在两个链接之间来回导航时(这两个链接不同),组件不应该再次被重建。如果你有复杂的图形和状态,并且你正在一个链接上修改它们,而当你返回重新开始工作时,对象被销毁并重新创建,会是怎样的情况?"
"框架不应该强制规定对象是否应该一遍又一遍地被销毁和重建。它应该提供两个选项,而在默认选项中,它不应该销毁和重建,因为这是直观的行为。这也是多年来大多数UI框架所使用的行为。"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接