Angular 7-我创建的订阅过多吗?

时间:2019-01-08 13:22:35

标签: angular

我想知道我的代码是否会造成内存泄漏?

上下文

我有一个应显示“应用程序”对象的组件类。 它具有过滤和分页功能。

我创建了一个方法 loadAppsData(),在其中我订阅 向Web服务发出请求后返回的Observable。

在初始化时 ngOnInit(),或者在用户与过滤输入字段或分页器交互之后调用此方法(请参见方法 onUserInteractionsWithTree()))< / p>

我的问题

为避免内存泄漏,我已经使用

.pipe(takeUntil(this.ngUnsubscribe))

ngOnDestroy(): void {
  this.ngUnsubscribe.next(); //  Unsubscribe from observables.
  this.ngUnsubscribe.complete(); // Unsubscribe from ngUnsubscribe.
}

但是在我看来,每当我发送一个请求到服务器时,当我调用subscribe()方法时,我都会创建一个新的Subscription对象。 这会造成内存泄漏吗? 我应该尝试重用订阅对象吗?

在此先感谢您的帮助,

在我组件的Typescript代码下面

import {Component, OnInit, ViewChild, ElementRef, OnDestroy} from '@angular/core';
import {FlatTreeControl} from '@angular/cdk/tree';

import {MatPaginator} from '@angular/material';

import {DynamicFlatNode} from './dynamic-flat-node';
import {ApplicationService} from '../shared/service/application-service';
import {DataRequestOptions} from '../../shared/data/data-request-options';
import {MetaDescriptor} from '../../shared/data/meta/meta-descriptor';
import {TableDataRequestParamsService} from '../../shared/data/table-data-request-params.service';


import {ApplicationTreeDatabase} from './application-tree-database';
import {ApplicationTreeDatasource} from './application-tree-datasource';

// Observable classes and extensions.
import {BehaviorSubject, Subject, fromEvent, of, merge} from 'rxjs';
// Observable operators.
import {debounceTime, distinctUntilChanged, switchMap, takeUntil} from 'rxjs/operators';


@Component({
  selector: 'app-application-tree',
  templateUrl: './application-tree.component.html',
  styleUrls: ['./application-tree.component.css'],
  providers: [ApplicationTreeDatabase]
})
export class ApplicationTreeComponent implements OnInit, OnDestroy {

  @ViewChild('appfilter') inputfilter: ElementRef;
  @ViewChild(MatPaginator) paginator: MatPaginator;

  readonly defaultPaginatorPageIndex = 0;
  readonly defaultPaginatorPageSize = 2;
  readonly defaultPaginatorPageRange = this.defaultPaginatorPageIndex + '-' + (this.defaultPaginatorPageSize - 1);


  private ngUnsubscribe: Subject<void> = new Subject<void>();

  // Application name filter. START
  _inputFilterChange = new BehaviorSubject('');
  get inputFilterValue(): string {
    return this._inputFilterChange.value;
  }
  set inputFilterValue(inputFilterValue: string) {
    this._inputFilterChange.next(inputFilterValue);
  }
  // Application name filter. END

  treeControl: FlatTreeControl<DynamicFlatNode>;

  dataSource: ApplicationTreeDatasource;

  getLevel = (node: DynamicFlatNode) => node.level;

  isExpandable = (node: DynamicFlatNode) => node.expandable;

  hasChild = (_: number, _nodeData: DynamicFlatNode) => _nodeData.expandable;


  constructor(
    private applicationService: ApplicationService,
    private dataRequestHelper: TableDataRequestParamsService,
    private database: ApplicationTreeDatabase) {
    this.treeControl = new FlatTreeControl<DynamicFlatNode>(this.getLevel, this.isExpandable);
    this.dataSource = new ApplicationTreeDatasource(this.treeControl, this.paginator, database);
  }


  ngOnInit(): void {
    fromEvent(this.inputfilter.nativeElement, 'keyup').pipe(
      debounceTime(150)
      , distinctUntilChanged()
      , switchMap(term => of(term))
      , takeUntil(this.ngUnsubscribe)
    )
      .subscribe(() => {
        if (!this.dataSource) {
          return;
        }
        // this.resetPaginator();
        this.inputFilterValue = this.inputfilter.nativeElement.value;
      });
    this.loadAppsData();
    this.onUserInteractionsWithTree();
  }
  ngOnDestroy(): void {
    this.ngUnsubscribe.next(); //  Unsubscribe from observables.
    this.ngUnsubscribe.complete(); // Unsubscribe from ngUnsubscribe.
  }

  resetFilterAndTriggerChange() {
    // Clear HTML filter content.
    this.inputfilter.nativeElement.value = '';
    // Clear filter data stream. => This will trigger database.load()
    // because of Event emmited by inputFilterValueChange.
    this.inputFilterValue = '';
  }

  buildAppDataRequestParams(): DataRequestOptions {
    let range = this.dataRequestHelper.buildRequestRangeValue(this.paginator);
    if (!range) { // paginator not initialized.
      range = this.defaultPaginatorPageRange;
    }
    return new DataRequestOptions(this.inputFilterValue, 'name', range);
  }

  private loadAppsData() {
    this.applicationService.getDataObjects(this.buildAppDataRequestParams())
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe(dataAndMeta => {
        // Update local Apps database.
        this.database.updateApplicationData(dataAndMeta.data);
        this.updatePaginator(dataAndMeta.meta);
        // Inform datasource that data has changed.
        this.dataSource.data = this.database.getAppsAsRootLevelNodes();
      },
      error => {
        const errMsg = 'Echec d\'acces aux données';
        throw new Error(errMsg);
      }
      );
  }

  private onUserInteractionsWithTree() {
    const treeUserActionsListener = [
      this._inputFilterChange,
      this.paginator.page
    ];
    // Merge the array of Observable inputs of treeUserActionsListener
    // and put into the source property of a newly created Observable.
    const mergeOfObservables = merge(...treeUserActionsListener);
    // Create new Observable<RoleMemberClient[]> by calling the function defined below.
    mergeOfObservables
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe((data: any) => {
        this.loadAppsData();
      });
  }

  private updatePaginator(meta: MetaDescriptor) {
    if ((meta) && (meta.isPaginatedData)) {
      const contentRange = meta.contentRange;
      const rangeStart = contentRange.rangeStart;
      this.paginator.pageIndex = Math.floor(rangeStart / this.paginator.pageSize);
      this.paginator.length = contentRange.size;
    } else if (meta) {
      // All data can be contained within the first table page.

      this.paginator.length = meta.count;
      if (this.paginator.pageIndex * this.paginator.pageSize < meta.count) {
        // If last requested page do not contain data, do not reset table page index.
        // The user will do it by itself.

        // Otherwise reset the table page index to zero.
        this.paginator.pageIndex = 0;
      }

    }
  }

}

4 个答案:

答案 0 :(得分:1)

内存泄漏:

可观察的模式易于发生内存泄漏,因为在组件(在这种情况下)失效之后将继续存在的预订将在应用程序的生命周期中持续存在。

例如:假设您有一个组件,该组件在创建时会订阅formControl,但每次创建时都不会关闭订阅您创建新订阅的组件。您有泄漏,可能会使内存超载。

关闭订阅:

可观察的完成或您手动取消订阅后,订阅结束。
您选择创建一个Subject(您将其命名为ngUnsubscribe-这是一个非常糟糕的名字)。当组件销毁时,您complete()是主题。
这意味着在销毁组件时将关闭对该主题的所有订阅。
订阅时,实际上您使用takeUntil(ngUnsubscribe),创建的是原始可观察的镜像并在该镜像上进行订阅。
因此,销毁组件时,在镜像(ngUnsubscribe)上进行的所有预订都会销毁。 不,您没有内存泄漏。

注释:
由于可观察的完成时订阅已关闭,因此您无需根据完成可观察的方法(例如角度HttpClientget,{{ 1}},...)。

您可以在Alex Beugnet的评论中提供的链接中找到所有这些信息。
为了更好的理解,您可以检查:

rxmarbles
learn-rxjs

答案 1 :(得分:1)

您的工作可以,但是可以,但是理想情况下,您应该使用模板内的| async管道进行订阅,因为angular可以处理所有订阅并为您取消订阅。孤儿订阅将导致应用程序中的内存泄漏。

因此在您的组件中执行此操作

ngOnInit() {
           this.dataObjects = this.applicationService.getDataObjects(this.buildAppDataRequestParams());
        }

并在您的模板中

<ng-container *ngFor="let dataObject of dataObjects | async">
    // Your html markup for each dataObject here 
    {{dataObject | json}}
 </ng-container>

如果要进行复杂的映射或事件,则应考虑使用BehaviourSubjects,然后使用

getDataObjects.toPromise().then(r => 
{
  dataObjectSubject.next(r.map(i => // mapping here))
})

并使用|async管道以相同的方式订阅模板中的BehaviourSubject。

希望有帮助

答案 2 :(得分:0)

大代码段,我注意到的很少:),但是很难实现整体和平。

1。

this.applicationService.getDataObjects(this.buildAppDataRequestParams())
      .pipe(takeUntil(this.ngUnsubscribe))

这里看起来不需要pipe(takeUntil(this.ngUnsubscribe)),因为getDataObjects看起来像xmlhttprequest。这样就立即完成了。

  1. switchMap(term => of(term))-对我来说很糟糕。您创建流并立即对其进行扁平化,我想主要目的是在新值出现时杀死流,但效果不好。

  2. .subscribe((data: any) => { this.loadAppsData();});-在订阅中,您调用了另一个函数,该函数也订阅了另一个流,因此很难遵循。

我会让您的组件更多地驱动事件。

将逻辑分解为何时需要触发数据,何时到达新数据需要发生什么。订阅一次。

想法:

this.data$ = merge(componentInit$, inputChanged$, nextPage$).pipe(
   switchMap(() => loadData()/* should return stream, not subscription */)
)
//...

 this.data$.pipe(takeUntil(/*bablba*/), map(/* transformations */)).subscribe(() => {
    //actions with data
 })

答案 3 :(得分:0)

首先

this.ngUnsubscribe.next(); //  Unsubscribe from observables.
  

next() 取消对可观察对象的订阅。用于通过可观察对象发送数据。

要接收此数据,您需要先订阅“ ngUnsuscribe”(坏名,因为它会产生误会),然后再调用函数。

因此,正确的主题流是

  1. 创建主题(私有subjectName:Subject = new Subject();)
  2. 订阅该主题。 (this.subjectName.subscribe())
  3. 通过此主题发送数据。 (this.subjectName.next(“ Test”))
  4. 使用后关闭主体。 (this.subjectName.complete())

如果在发送数据的下一行中关闭主题,则可能会由于主题通信的异步性质而产生问题。