如何在Angular 2中重新触发所有组件树上的所有纯管道

Posted

技术标签:

【中文标题】如何在Angular 2中重新触发所有组件树上的所有纯管道【英文标题】:How to re-trigger all pure pipes on all component tree in Angular 2 【发布时间】:2017-06-11 16:17:23 【问题描述】:

我有纯管道TranslatePipe,它使用LocaleService 翻译短语,该locale$: Observable<string> 当前语言环境。我还为我的所有组件启用了ChangeDetectionStrategy.OnPush,包括AppComponent。现在,当有人更改语言时,我如何重新加载整个应用程序? (在locale$ observable 中发出新值)。

目前,我在用户切换语言后使用location.reload()。这很烦人,因为整个页面都被重新加载了。如何使用 pure 管道和 OnPush 检测策略来实现这种角度方式?

【问题讨论】:

【参考方案1】:

纯管道仅在输入值变化时触发。

你可以添加一个你修改的人为的附加参数值

@Pipe(name: 'translate')
export class TranslatePipe 
  transform(value:any, trigger:number) 
    ...
  

然后像这样使用它

<div>label | translate:dummyCounter</div>

每当dummyCounter 更新时,都会执行管道。

您还可以将语言环境作为附加参数而不是计数器传递。 我不认为将|async 用于单个管道参数会起作用,因此这可能有点麻烦(需要分配给一个字段才能用作管道参数)

【讨论】:

谢谢,这是解决此问题的一种方法,但我的模板中有很多字符串和很多代码,例如 'This is some text' |翻译,所以有一个额外的参数,我必须把它放在任何地方都会伤害......有没有办法在不重新加载整个页面的情况下重新加载整个应用程序? 您可以在AppComponent 的最外层元素处放置一个ngIf="expr",将expr 更改为false,运行ChangeDetectorRef.detectChanges(),将其设置回true 并运行@987654331 @ 再次。我认为这是最简单的方法。这样,所有内容都将被删除和读取。您可能需要再次导航到当前路由(虽然不确定路由器对此有何反应,但我认为尝试一下就很容易了) 工作..!谢谢【参考方案2】:

最佳性能解决方案:

我想出了一个解决方案。我讨厌将其称为解决方案,但它确实有效。

我在使用 orderBy 管道时遇到了同样的问题。我在这里尝试了所有解决方案,但性能影响很糟糕。

我只是在管道中添加了一个附加参数

let i of someArray | groupBy:'someField':updated" 
<!--updated is updated after performing some function-->

然后每当我对数组执行更新时,我只需

updateArray()
    //this can be a service call or add, update or delete item in the array
      .then.....put this is in the callback:

    this.updated = new Date(); //this will update the pipe forcing it to re-render.

这迫使我的 orderBy 管道再次进行转换。而且性能要好很多。

【讨论】:

您先生刚刚拯救了我的一天。谢谢! :)【参考方案3】:

感谢 Günter Zöchbauer answer(参见 cmets),我得到了它的工作。

据我了解,Angular 的变化检测器是这样工作的:

cd.detectChanges(); // Detects changes but doesn't update view.
cd.markForCheck();  // Marks view for check but doesn't detect changes.

所以你需要同时使用这两者才能快速重建整个组件树。

1。模板更改

为了重新加载整个应用程序,我们需要隐藏和显示所有组件树,因此我们需要将 app.component.html 中的所有内容包装到 ng-container 中:

<ng-container *ngIf="!reloading">
  <header></header>
  <main>
    <router-outlet></router-outlet>
  </main>
  <footer></footer>
</ng-container>

ng-container 比 div 更好,因为它不渲染任何元素。

对于异步支持,我们可以这样做:

<ng-container *ngIf="!(reloading$ | async)"> ... </ng-container>

这里的reloading: booleanreloading$: Observable&lt;boolean&gt;表示当前正在重新加载组件。

在我有LocaleService 的组件中,language$ 是可观察的。我将监听更改的语言事件并执行应用程序重新加载操作。

2。同步示例

export class AppComponent implements OnInit 
    reloading: boolean;

    constructor(
        private cd: ChangeDetectorRef,
        private locale: LocaleService) 

        this.reloading = false;
    

    ngOnInit() 
        this.locale.language$.subscribe(_ => 
            this.reloading = true;
            this.cd.detectChanges();
            this.reloading = false;
            this.cd.detectChanges();
            this.cd.markForCheck();
        );
    

3。异步示例

export class AppComponent implements OnInit 
    reloading: BehaviorSubject<boolean>;

    get reloading$(): Observable<boolean> 
        return this.reloading.asObservable();
    

    constructor(
        private cd: ChangeDetectorRef, // We still have to use it.
        private locale: LocaleService) 

        this.reloading = new BehaviorSubject<boolean>(false);
    

    ngOnInit() 
        this.locale.language$.subscribe(_ => 
            this.reloading.next(true);
            this.cd.detectChanges();
            this.reloading.next(false);
            this.cd.detectChanges();
        );
    

我们现在不必cd.markForChanges(),但我们仍然需要告诉检测器检测变化。

4。路由器

路由器无法正常工作。以这种方式重新加载应用程序时,router-outlet 的内容将变为 empty。我还没有解决这个问题,走同样的路可能会很痛苦,因为这意味着用户在表单中所做的任何更改都会被更改并丢失。

5。 OnInit

您必须使用 OnInit 挂钩。如果你尝试在构造函数中调用 cd.detectChanges() ,你会得到一个错误,因为 Angular 还不会构建组件,但你会尝试检测它的变化。

现在,您可能认为我在构造函数中订阅了另一个服务,并且我的订阅只有在组件完全初始化后才会触发。但问题是 - 你不知道服务是如何运作的!例如,如果它只发出一个值 Observable.of('en') - 你会收到一个错误,因为一旦你订阅 - 第一个元素立即发出,而组件仍未初始化。

我的LocaleService 也有同样的问题:observable 后面的主题是BehaviorSubjectBehaviorSubject 是 rxjs 主题,在您订阅后立即发出 default 值。所以一旦你写了this.locale.language$.subscribe(...) - 订阅会立即触发至少一次,然后你才会等待语言更改。

【讨论】:

您可以使用&lt;ng-container *ngIf="!reloading"&gt;。添加它是为了能够使用更常见的语法,但仍具有与 &lt;template&gt; 元素相同的行为。 使用&lt;ng-container&gt;更新。 AFAIK cd.detectChanges(); 立即同步运行更改检测,cd.markForCheck(); 在下一个更改检测周期(稍后)运行此组件的更改检测。 当我尝试使用 cd.detectChanges() 而没有 cd.markForCheck() 最后 - 它只是不起作用。仅使用 cd.markForCheck() 也不起作用。 可能与路由器有关。在这种情况下,ApplicationRef.tick() 可能是为整个应用程序调用更改检测的更好方法。这甚至可能修复 4.【参考方案4】:

只需将属性 pure 设置为 false

@Pipe(
  name: 'callback',
  pure: false
)

【讨论】:

"Angular executes an impure pipe during every component change detection cycle. An impure pipe is called often, as often as every keystroke or mouse-move. With that concern in mind, implement an impure pipe with great care. An expensive, long-running pipe could destroy the user experience." @StinkyCat 检查翻译只是从 hashmap 中读取。会影响性能这么差吗? 这取决于您正在构建的网站,但如果它是一个巨大的网站,有很多绑定......是的,它可能会减慢体验速度。 (我的意见,根据经验,没有实际测量!) 完美的解决方案,它对我很有魅力!!【参考方案5】:

您还可以创建自己的非纯管道来跟踪外部更改。查看原生Async Pipe的来源了解主要思想。

每次您的 Observable 返回新的语言环境字符串时,您只需要在 unpure 管道内调用 ChangeDetectorRef.markForCheck();。我的解决方案:

@Pipe(
  name: 'translate',
  pure: false
)
export class TranslatePipe implements OnDestroy, PipeTransform 

  private subscription: Subscription;
  private lastInput: string;
  private lastOutput: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) 
    this.subscription = this.globalizationService.currentLocale // <- Observable of your current locale
      .subscribe(() => 
        this.lastOutput = this.globalizationService.translateSync(this.lastInput); // sync translate function, will return string
        this.changeDetectorRef.markForCheck();
      );
  

  ngOnDestroy(): void 
    if (this.subscription) 
      this.subscription.unsubscribe();
    
    this.subscription = void 0;
    this.lastInput = void 0;
    this.lastOutput = void 0;
  

  transform(id: string): string  // this function will be called VERY VERY often for unpure pipe. Be careful.
    if (this.lastInput !== id) 
      this.lastOutput = this.globalizationService.translateSync(id);
    
    this.lastInput = id;
    return this.lastOutput;
  

或者您甚至可以将 AsyncPipe 封装在您的管道中(这不是一个好的解决方案,仅举个例子):

@Pipe(
  name: 'translate',
  pure: false
)
export class TranslatePipe implements OnDestroy, PipeTransform 

  private asyncPipe: AsyncPipe;
  private observable: Observable<string>;
  private lastValue: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) 
    this.asyncPipe = new AsyncPipe(changeDetectorRef);
  

  ngOnDestroy(): void 
    this.asyncPipe.ngOnDestroy();
    this.lastValue = void 0;
    if (this.observable) 
      this.observable.unsubscribe();
    
    this.observable = void 0;
    this.asyncPipe = void 0;
  

  transform(id: string): string 
    if (this.lastValue !== id || !this.observable) 
      this.observable = this.globalizationService.translateObservable(id); // this function returns Observable
    
    this.lastValue = id;

    return this.asyncPipe.transform(this.observable);
  


【讨论】:

他特意要了一根纯烟斗。 布莱恩,我知道。但是 Async Pipe 解决方案足以创建翻译管道。至少,它在我的项目上运行良好。未发现任何性能问题。 markForCheck 在非纯管道中不是没有意义吗?甚至任何管道? @cambunctious,如果你的管道依赖的东西不是这个管道的参数(例如,来自本地化服务的应用程序区域设置),那么如果该参数发生变化,你需要强制管道再次调用transform() 方法。即使您的ChangeDetectionStrategyDefaultChangeDetectorRef.markForCheck() 会这样做。它将标记当前视图以重新检查更改,这将调用transform() 函数。 好的,我明白了。这是一个很好的解决方案,因为它可以防止组件必须使用 ChangeDetectorRef。 AsyncPipe 是一个很好的相关示例,因为它也是使用 ChangeDetectorRef 实现的,但您当然不需要使用它。另外需要注意的是,如果您希望管道在不更改输入的情况下更改其输出,则绝对需要使用不纯的管道。只要转换方法成本低,就可以,就像使用默认更改检测并经常从角度访问您的组件一样。【参考方案6】:

阅读此角度文档以检测将使用管道的数组的更改。

要解决此问题,请创建一个新数组(带有新更改)并将其分配给您要在管道中使用的数组。这次 Angular 检测到数组引用发生了变化。它执行管道并使用带有新更改的新数组更新显示

enter link description here

【讨论】:

以上是关于如何在Angular 2中重新触发所有组件树上的所有纯管道的主要内容,如果未能解决你的问题,请参考以下文章

如何从Angular 2中的子组件事件触发父组件中的本地引用?

如何在 Angular 2 中强制组件重新渲染?

如何在 Angular 2 中重新加载相同 URL 的组件?

在每个组件完成加载后如何在 Angular 2 中运行 jquery 函数

如何在Angular 2中选择ngFor中的所有过滤复选框?

React生命周期, setState、props改变触发的钩子函数