Angular observable никогда не завершается обработчиком

Ниже приведены фрагменты моего кода. Интересно, почему он НИКОГДА не сталкивается с обработчиком завершения? Что я хотел сделать, так это сначала вызвать службу A, чтобы получить объект A с заданным параметром маршрута ['key'], а затем вызвать службу B, чтобы получить объект B. Итак, objectB зависит от результата objectA, который зависит от заданного параметра ['key'].

p.s. Я использую Angular 7 с rxjs6.

ngOnInit() {
    this.route.params.pipe(
      mergeMap(
        (params: Params) => {
          this.key = params.key;
          return this.serviceA.getObjectA(this.key);    // http request service to backend
        }
      ),
      mergeMap(
        (objectA: ObjectA) => {
          // do something with objectA

          return this.serviceB.getListOfObjectB();  // http request service to backend
        }
      )
    ).subscribe(
      (objectBList: ObjectB[]) => {
        for (const b of objectBList) {
          // do something with objectB
        }
        // the code execution ends here
      },
      () => {
        // error handler
      },
      () => {
        // completion handler
        // the code execution NEVER comes to here, WHY??
      }
    );
  }

person Angus    schedule 08.06.2020    source источник


Ответы (1)


Я предполагаю, что route является введенным ActivatedRoute.

Каждый ActivatedRoute связан с маршрутизируемым компонентом, и когда происходит изменение маршрута, текущий отображаемый компонент будет уничтожен, как и связанный с ним ActivatedRoute, вот почему вы не получите уведомление complete.


вот как ActivatedRoute создано:

function createActivatedRoute(c: ActivatedRouteSnapshot) {
  return new ActivatedRoute(
      new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
      new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
}

Теперь, как ActivatedRoute связаны с маршрутизируемым компонентом?

Предполагая, что у вас есть конфигурация, которая выглядит следующим образом:

{
  path: 'a/:id',
  component: AComponent,
  children: [
    {
      path: 'b',
      component: BComponent,
    },
    {
      path: 'c',
      component: CComponent,
    },
  ]
}

и выданный URL, например a/123/b

в итоге вы получите дерево из ActivatedRoutes:

      APP
       |
       A
       |
       B

Всякий раз, когда вы планируете навигацию (например, router.navigateToUrl()), она должна пройти несколько важных этапов:

  • применить перенаправления: проверка перенаправлений; загрузка отложенных модулей; найти NoMatch ошибок
  • распознать: создание дерева ActivatedRouteSnapshot
  • предварительная активация: сравнение полученного дерева с текущим; на этом этапе также собираются canActivate и canDeactivate охранников на основе найденных различий
  • бегущие охранники
  • создать состояние маршрутизатора: где создается дерево ActivatedRoute
  • активация маршрутов: это вишенка на торте и место, где используется дерево ActivatedRoute.

    Также важно упомянуть роль, которую играет router-outlet.

    Angular отслеживает router-outlet с помощью объекта Map.

    вот что происходит, когда у вас есть <router-outlet></router-outlet> в вашем приложении:

@Directive({selector: 'router-outlet', exportAs: 'outlet'})
export class RouterOutlet implements OnDestroy, OnInit {
  private activated: ComponentRef<any>|null = null;
  private _activatedRoute: ActivatedRoute|null = null;
  private name: string;

  @Output('activate') activateEvents = new EventEmitter<any>();
  @Output('deactivate') deactivateEvents = new EventEmitter<any>();

  constructor(
      private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
      private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
      private changeDetector: ChangeDetectorRef) {
    this.name = name || PRIMARY_OUTLET;
    parentContexts.onChildOutletCreated(this.name, this);
  }
}

Обратите внимание на наличие activated (это компонент) и _activatedRoute!.

И здесь находятся соответствующие биты из ChildrenOutletContexts:

export class ChildrenOutletContexts {
  // contexts for child outlets, by name.
  private contexts = new Map<string, OutletContext>();

  /** Called when a `RouterOutlet` directive is instantiated */
  onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
    const context = this.getOrCreateContext(childName);
    context.outlet = outlet;
    this.contexts.set(childName, context);
  }
}

где childName по умолчанию 'primary'. Пока сосредоточьте свое внимание только на части context.outlet.

Итак, для нашей конфигурации маршрута:

{
  path: 'a/:id',
  component: AComponent,
  children: [
    {
      path: 'b',
      component: BComponent,
    },
    {
      path: 'c',
      component: CComponent,
    },
  ]
}

router-outlet Map будет выглядеть так (примерно):

{
  primary: { // Where `AComponent` resides [1]
    children: {
      // Here `AComponent`'s children reside [2]
      primary: { children: { } }
    }
  }
}

Теперь давайте посмотрим, как маршрут активирован:

// This block of code will be run for [1] and [2] (in this order!)
const context = parentContexts.getOrCreateContext(future.outlet);
/* ... */

const config = parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;

context.attachRef = null;
context.route = future;
context.resolver = cmpFactoryResolver;
if (context.outlet) {
  context.outlet.activateWith(future, cmpFactoryResolver);
}

this.activateChildRoutes(futureNode, null, context.children);

context.outlet.activateWith(future, cmpFactoryResolver); что мы ищем (где outlet — экземпляр директивы RouterOutlet):

  activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
    if (this.isActivated) {
      throw new Error('Cannot activate an already activated outlet');
    }
    this._activatedRoute = activatedRoute;
    const snapshot = activatedRoute._futureSnapshot;
    const component = <any>snapshot.routeConfig!.component;
    resolver = resolver || this.resolver;
    const factory = resolver.resolveComponentFactory(component);
    const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
    const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
    this.activated = this.location.createComponent(factory, this.location.length, injector);
    // Calling `markForCheck` to make sure we will run the change detection when the
    // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
    this.changeDetector.markForCheck();
    this.activateEvents.emit(this.activated.instance);
}

Обратите внимание, что this.activated содержит маршрутизируемый компонент (например, AComponent), а this._activatedRoute содержит ActivatedRoute для этого компонента.

Теперь посмотрим, что происходит когда мы переходим к другому маршруту и ​​это текущее представление уничтожается:

deactivateRouteAndOutlet(
    route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
  const context = parentContexts.getContext(route.value.outlet);

  if (context) {
    const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
    const contexts = route.value.component ? context.children : parentContexts;

    // Deactivate children first
    forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts));

    if (context.outlet) {
      // Destroy the component
      context.outlet.deactivate();
      // Destroy the contexts for all the outlets that were in the component
      context.children.onOutletDeactivated();
    }
  }
}

где RouterOutlet.deactivate() выглядит вот так< /а>:

deactivate(): void {
  if (this.activated) {
    const c = this.component;
    this.activated.destroy(); // Destroying the current component
    this.activated = null;
    // Nulling out the activated route - so no `complete` notification
    this._activatedRoute = null;
    this.deactivateEvents.emit(c);
  }
}
person Andrei Gătej    schedule 08.06.2020