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,
});
};
}
注意 我在
模板:
<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,
});
};
}