Angular 模板绑定与 Observable 异步管道问题

Angular template binding with Observable async pipe issue

注意 我在

创建了这个问题的简化版本

模板:

<div *ngIf="entity?.ext.insuredDetails.insuredType$() | async as insuredType">
 {{insuredType}}
</div>

insuredType$定义:

@NeedsElement(sp(115621),ap(116215))
insuredType$(): Observable<string> {
  return empty();
}

NeedsElement装饰者:

export function NeedsElement(...mappings: NeedsElementMapping[]) {
  if (mappings.length === 0) {
    throw new Error('needs mapping expected');
  }

  let lookup = new Map<ProductId, number>();
  mappings.forEach((mapping) => {
    lookup.set(mapping.productId, mapping.elementId);
  });

  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    descriptor.value = function (...args: any[]) {
      Logger.info("bbbbb");
      let entity = UcEntityStoreContext.currentEntity;
      let productId = entity['productId'];
      if (!productId) {
        throw new Error(`Cannot get product Id from host entity: ${entity.ucId}`);
      }
      let elementId: number = lookup.get(entity['productId']);
      if (!elementId) {
        throw new Error(`Cannot locate needs element ID by productId ${productId}`);
      };
      let enitityStore = UcEntityStoreContext.current;
      let entityApi = enitityStore.apiService as QuotePolicyApiBase<any>;
      let needsDefApi = NeedsDefinitionApi.instance;

      return needsDefApi.fetchOne(productId, elementId).pipe(
        concatMap(
          nd => {
            return entityApi.fetchNeedsElementValue(entity.ucId, elementId).pipe(
              concatMap(needsVal => {
                if (!needsVal) {
                  return of("");
                }
                if (nd.lookupId) {
                  return LookupApi.instance.getByPrimaryValueId(nd.lookupId, needsVal).pipe(
                    map(res => res.primaryValue)
                  );
                } else {
                  return of(needsVal);
                }
              })
            )
          }
        )
      );
    };
  };
}

问题是装饰器被多次调用:

如果它进入这个分支:

然后它继续向后端服务发送请求并且绑定从不输出任何内容:

看起来它会一直尝试评估可观察对象,如果它是异步可观察对象,则不会结束,比如说:


更新 14/May/2020

我从

那里得到了答案

最后我将 Method Decorator 更改为 属性 Decorator 并修复了问题。

当您使用 insuredType$() | async 之类的东西时,这意味着 angular 每次发生变化检测时都会调用此函数。因此它每次都会调用 needsDefApi.fetchOne(productId, elementId)

要避免它,您需要标记您的组件 OnPush。减少调用量实际上是一种生活技巧,因为只有在组件的输入发生变化或触发输出的情况下才会调用它。如果它经常发生 - 它不会帮助。

或者您需要在对相同 entity 的任何调用中将装饰器重组为 return 相同 Observable,因此 entity?.ext.insuredDetails.insuredType$() === entity?.ext.insuredDetails.insuredType$() 为真。

不确定它是否有效,但应该与它相似:

export function NeedsElement(...mappings: NeedsElementMapping[]) {
    if (mappings.length === 0) {
        throw new Error('needs mapping expected');
    }

    let lookup = new Map<ProductId, number>();
    mappings.forEach((mapping) => {
        lookup.set(mapping.productId, mapping.elementId);
    });

    Logger.info("bbbbb");
    let entity = UcEntityStoreContext.currentEntity;
    let productId = entity['productId'];
    if (!productId) {
        throw new Error(`Cannot get product Id from host entity: ${entity.ucId}`);
    }
    let elementId: number = lookup.get(entity['productId']);
    if (!elementId) {
        throw new Error(`Cannot locate needs element ID by productId ${productId}`);
    };
    let enitityStore = UcEntityStoreContext.current;
    let entityApi = enitityStore.apiService as QuotePolicyApiBase<any>;
    let needsDefApi = NeedsDefinitionApi.instance;

    const stream$ = needsDefApi.fetchOne(productId, elementId).pipe(
        concatMap(
            nd => {
                return entityApi.fetchNeedsElementValue(entity.ucId, elementId).pipe(
                    concatMap(needsVal => {
                        if (!needsVal) {
                            return of("");
                        }
                        if (nd.lookupId) {
                            return LookupApi.instance.getByPrimaryValueId(nd.lookupId, needsVal).pipe(
                                map(res => res.primaryValue)
                            );
                        } else {
                            return of(needsVal);
                        }
                    })
                )
            }
        )
    );

    return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
        descriptor.value = function (...args: any[]) {
            return stream$; // <- returns the same stream every time.
        };
    };
}

得到答案

解决方案是使用 属性 装饰器而不是方法装饰器,因此 insuredType$ 现在是:

@NeedsElement(sp(115623),ap(116215))
readonly insuredType$: Observable<any>;

装饰器现在

export function NeedsElement(...mappings: NeedsElementMapping[]) {
  ...
  const observable = of('').pipe(switchMap(() => {
    ...
  })
  return (target: any, propertyKey: string) => {
    const getter = () => {
      return observable;
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      enumerable: true,
      configurable: true,
    });
  };
}

注意必须在返回函数外定义observable,否则还是会陷入死循环,下面说说代码无效:

export function NeedsElement(...mappings: NeedsElementMapping[]) {
  ...
  return (target: any, propertyKey: string) => {
    const getter = () => {
      return of('').pipe(switchMap(() => {
        ...
      });
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      enumerable: true,
      configurable: true,
    });
  };
}