Angular: eine andere Möglichkeit, sich abzumelden

Abonnements im Komponentencode sollten vermieden werden, indem diese Aufgabe auf AsyncPipe verschoben wird. Dies ist jedoch nicht immer möglich. Es gibt verschiedene Möglichkeiten, Abonnements zu beenden, aber alle bestehen aus zwei Möglichkeiten: manuelles Abbestellen oder Verwenden von takeUntil.


Mit der Zeit begann ich zunehmend, meinen Dekorateur zum Abbestellen zu verwenden. Lassen Sie uns überlegen, wie es angeordnet und angewendet wird. Vielleicht wird Ihnen diese Methode gefallen.


Die Grundidee ist, dass jedes Abonnement von einer Methode zurückgegeben werden sollte. Jene. Alle Abonnements werden in einer separaten dekorierten Methode ausgeführt und haben die folgende Signatur.


(...args: any[]) => Subscription;

Mit Typescript können Sie einen Dekorateur an eine Methode hängen, mit der die Methode und ihr Ergebnis geändert werden können.


Drei Operationen sind erforderlich.


  1. Beim Aufrufen von ngOnInit sollte eine Art Abonnement-Repository erstellt werden.
  2. Wenn Sie eine dekorierte Methode aufrufen, die ein Abonnement zurückgibt, muss dieses Abonnement im Repository gespeichert werden.
  3. Wenn ngOnDestroy aufgerufen wird, müssen alle Abonnements aus dem Repository abgeschlossen sein (Abbestellen).

Ich möchte Sie daran erinnern, wie ein Klassenmethoden-Dekorateur hergestellt wird. Die offizielle Dokumentation finden Sie hier


Hier ist die Unterschrift des Dekorateurs:


<T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void; 

Der Dekorator erhält den Klassenkonstruktor, den Namen der Eigenschaft und einen Deskriptor als Eingabe. Ich werde den Deskriptor ignorieren, er spielt für diese Aufgabe keine Rolle, kaum jemand wird den Methodendeskriptor modifizieren.


, , - , , .


,


export function UntilOnDestroy<ClassType extends DirectiveWithSubscription>(): MethodDecorator {
  return function UntilOnDestroyDecorator(target: ClassType, propertyKey: string): TypedPropertyDescriptor<SubscriptionMethod> {
    wrapHooks(target);
    return {
      value: createMethodWrapper(target, target[propertyKey]),
    };
  } as MethodDecorator;
}

, , , — - , , createMethodWrapper.


, .1 . 3 (Subscription). add, unsubscribe .


Subscription


-, .


const subSymbol = Symbol('until-on-destroy');

interface ClassWithSubscription {
  [subSymbol]?: Subscription;
}

, .


createMethodWrapper


2.


function createMethodWrapper(target: ClassWithSubscription, originalMethod: SubscriptionMethod): SubscriptionMethod {
  return function(...args: any[]) {
    const sub: Subscription = originalMethod.apply(this, args);
    target[subSymbol].add(sub);
    return sub;
  };
}

createMethodWrapper , , () . subSymbol, , .


wrapHooks


1 3.


function wrapHooks(target: ClassWithSubscription) {
  if (!target.hasOwnProperty(subSymbol)) {
    target[subSymbol] = null;
    wrapOneHook(target, 'OnInit', t => t[subSymbol] = new Subscription());
    wrapOneHook(target, 'OnDestroy', t => t[subSymbol].unsubscribe());
  }
}

, , subSymbol.


. , , .


. , Angular 9 , . ViewEngine Ivy


const cmpKey = 'ɵcmp';

function wrapOneHook(target: any, hookName: string, wrappingFn: (target: ClassWithSubscription) => void): void {
  return target.constructor[cmpKey]
    ? wrapOneHook__Ivy(target, hookName, wrappingFn)
    : wrapOneHook__ViewEngine(target, hookName, wrappingFn);
}

'ɵcmp' , Ivy . Ivy .


ViewEngine


function wrapOneHook__ViewEngine(target: any, hookName: string, wrappingFn: (target: ClassWithSubscription) => void): void {
  const veHookName = 'ng' + hookName;
  if (!target[veHookName]) {
    throw new Error(`You have to implements ${veHookName} in component ${target.constructor.name}`);
  }
  const originalHook: () => void = target[veHookName];
  target[veHookName] = function (): void {
    wrappingFn(target);
    originalHook.call(this);
  };
}

, ( ), .


wrappingFn.


Ivy , Ivy. , .


ngOnInit ngOnDestroy.


function wrapOneHook__Ivy(target: any, hookName: string, wrappingFn: (target: ClassWithSubscription) => void): void {
  const ivyHookName = hookName.slice(0, 1).toLowerCase() + hookName.slice(1);
  const componentDef: any = target.constructor[cmpKey];

  const originalHook: () => void = componentDef[ivyHookName];
  componentDef[ivyHookName] = function (): void {
    wrappingFn(target);

    if (originalHook) {
      originalHook.call(this);
    }
  };
}

, , componentDef.


, , OnInit ngOnInit, onInit.


, .




ng new check


ng g c child


app.component.ts


@Component({
  selector: 'app-root',
  template: `
    <button #b (click)="b.toggle = !b.toggle">toggle</button>
    <app-child *ngIf="b.toggle"></app-child>
  `,
})
export class AppComponent {}

child.component.ts


@Component({
  selector: 'app-child',
  template: `<p>child: {{id}}</p>`,
})
export class ChildComponent implements OnInit {
  id: string;

  ngOnInit() {
    this.id = Math.random().toString().slice(-3);
    this.sub1();
    this.sub2();
  }

  @UntilOnDestroy()
  sub1(): Subscription {
    console.log(this.id, 'sub1 subscribe');
    return NEVER.pipe(
      finalize(() => console.log(this.id, 'sub1 unsubscribe'))
    )
      .subscribe();
  }

  sub2(): Subscription {
    console.log(this.id, 'sub2 subscribe');
    return NEVER.pipe(
      finalize(() => console.log(this.id, 'sub2 unsubscribe'))
    )
      .subscribe();
  }
}

toggle app-child :



… :



sub1 , sub2 .


.


Stackblitz Link
für Angular 9 auf GitHub


Decorator kann in npm als Paket ngx-until-on-destroy
Decorator-Quellen auf Github verwendet werden


All Articles