Angular: une autre façon de se désinscrire

Les abonnements dans le code du composant doivent être évités en déplaçant cette tâche vers AsyncPipe, mais ce n'est pas toujours possible. Il existe différentes manières de mettre fin aux abonnements, mais elles se résument toutes à deux: la désinscription manuelle ou l'utilisation de takeUntil.


Au fil du temps, j'ai de plus en plus utilisé mon décorateur pour me désinscrire. Voyons comment il est organisé et appliqué, vous aimerez peut-être cette méthode.


L'idée de base est que tout abonnement doit être renvoyé à partir d'une méthode. Ceux. tous les abonnements se produisent dans une méthode décorée distincte et il a la signature suivante.


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

Typescript vous permet de suspendre un décorateur sur une méthode qui peut modifier la méthode et son résultat.


Trois opérations seront nécessaires.


  1. Lors de l'appel à ngOnInit, une sorte de référentiel d'abonnement doit être créé.
  2. Lors de l'appel d'une méthode décorée qui renvoie un abonnement, cet abonnement doit être stocké dans le référentiel.
  3. Lorsque ngOnDestroy est appelé, tous les abonnements du référentiel doivent être terminés (se désinscrire).

Permettez-moi de vous rappeler comment un décorateur de méthode de classe est fait. La documentation officielle est ici


Voici la signature du décorateur:


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

Le décorateur reçoit le constructeur de classe, le nom de la propriété et un descripteur en entrée. J'ignorerai le descripteur, il ne joue aucun rôle pour cette tâche, presque personne ne modifiera le descripteur de méthode.


, , - , , .


,


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 .


.


Lien Stackblitz
pour Angular 9 sur GitHub


Decorator peut être pris en npm en tant que ngx-until-on-destroy
source de décorateur sur Github


All Articles