实践

优雅的资源释放

对于无限值必须要取消订阅,反之可以不需要。例如监听 DOM 元素的事件:

Observable.fromEvent(node, 'input')
  .subscribe(value => {
      console.log(value);
  });

因为如果不取消订阅,事件所关联的方法会一直被占用,导致内存泄露。

传统方式

@Component({
  selector: 'app-demo',
  template: `
  <div>Hello, world!</div>
  `
})
export class GeneralComponent implements OnDestroy {
  private readonly _destroy$ = new Subject<void>();

  private timer; 

  constructor() {
    this.timer = interval(1000).pipe(takeUntil(this._destroy$)).subscribe(console.log);
  }

  ngOnDestroy() {
    this._destroy$.next();
    this._destroy$.complete();
  }
}

上面的在组件中定义了一个私有变量 _destroy$,是一个 Subject 对象,用于在组件销毁时发出信号以释放资源。通过 takeUntil(this._destroy$) 操作符来限制 Observable 的生命周期,在 _destroy$ 发出信号时停止发出值。

这种方式虽然使用了 takeUntil 来限制 Observable 的生命周期,但是仍然需要在 ngOnDestroy 钩子中手动调用 _destroy$.next()_destroy$.complete() 来确保释放资源。容易遗漏而引发错误。

继承方式

@Directive()
export class BaseComponent implements OnDestroy {
  // protected, not private
  protected readonly _destroy$ = new Subject<void>();

  ngOnDestroy() {
    this._destroy$.next();
    this._destroy$.complete();
  }
}

@Component({
  selector: 'app-demo',
  template: `
  <div>Hello, world!</div>
  `
})
export class GeneralComponent extends BaseComponent {
  constructor() {
    super();

    interval(1000).pipe(takeUntil(this._destroy$)).subscribe(console.log);
  }
}

继承方式可以减少了在每个组件中手动管理资源释放的重复性工作。但是导致了派生组件与基类紧密耦合。一个派生类只能继承自一个基类。如果要在不同的组件中共享不同的基础逻辑,就会受到继承单一基类的限制。而且继承方式导致代码的可读性和可维护性下降。

DestroyRef 机制

function destroyRefFactory() {
  const destroy$ = new Subject<void>();
  const destroyRef = inject(DestroyRef);

  destroyRef.onDestroy(() => {
    destroy$.next();
    destroy$.complete();
  })

  return destroy$;
}

@Component({
  selector: 'app-demo',
  template: `
  <div>Hello, world!</div>
  `
})
export class GeneralComponent implements OnDestroy {
  private readonly _destroy$ = destroyRefFactory();

  constructor() {
    interval(1000).pipe(takeUntil(this._destroy$)).subscribe(console.log)
  }
}

基于 DestroyRef 机制,不需要在组件中手动释放资源。而且不仅限于单一订阅场景,它在多个订阅场景中同样适用。

自定义操作符

基于 DestroyRef 机制的实现方式简洁灵活,但是仍然需要在组件中声明 _destroy$。通过自定义操作符可以将释放资源的逻辑封装在操作符内部,让代码更加整洁,使资源释放与业务逻辑解耦。

function takeUntilDestroyed<T>(destroyRef?: DestroyRef): MonoTypeOperatorFunction<T> {
  if (!destroyRef) {
    destroyRef = inject(DestroyRef);
  }

  const destroy$ = new Observable<void>(observer => {
    return destroyRef!.onDestroy(() => {
      observer.next();
      observer.complete();
    });
  })

  return <T>(source: Observable<T>) => {
    return source.pipe(takeUntil(destroy$))
  }
}

@Component({
  selector: 'app-demo',
  template: `
  <div>Hello, world!</div>
  `
})
export class GeneralComponent implements OnDestroy {
  constructor() {
    interval(1000).pipe(takeUntilDestroyed()).subscribe(console.log)
  }
}

@angular/core/rxjs-interop 中已经提供了 takeUntilDestroyed 操作符。

最后更新于