为什么Angular中BehaviorSubject订阅后会多次触发回调?
问题背景
现有3个组件:
- InvoicelistComponent
- CreateinvoiceComponent
- HeaderInvoiceComponent
InvoicelistComponent 跳转逻辑
提供跳转到CreateInvoice页面的方法,代码如下:
getInvoiceDetail(invoiceId: number) { this.router.navigate('mainmenu/invoice/createinvoice', invoiceId) }
CreateinvoiceComponent 数据拉取逻辑
在ngOnInit中获取invoiceId,调用API拉取数据后存入服务,代码如下:
ngOnInit() { let checkMode = this.routerService.data === undefined || null ? false : true; if (checkMode === true) { this.spinner.show(); let invoiceId = this.routerService.data; this.createInvoiceService.getdatainvoicebyid(invoiceId).subscribe(res => { let invoicemodel = new Invoice(); invoicemodel = res; this.searchService.setdatainvoiceData(invoicemodel['result'][0]); this.spinner.hide(); }); } }
SearchService 跨组件传值逻辑
使用BehaviorSubject传递数据,相关代码如下:
public setInvoiceData = new BehaviorSubject<any>(null); setdatainvoiceData(data: any) { this.setInvoiceData.next(data); }
HeaderInvoiceComponent 数据订阅逻辑
在构造函数中订阅该BehaviorSubject获取数据,代码如下:
this.searchService.setInvoiceData.subscribe(res => { if (res != null || res != undefined) { console.log(res); this.objRequestParam.InvoiceType = res.Invoiceheader.invoicetypeid this.objRequestParam.InvoiceSubType = res.Invoiceheader.invoicesubtypeid this.objRequestParam.InvoiceDate = res.Invoiceheader.invoicedate; this.objRequestParam.txtInvoiceNo = res.Invoiceheader.invoiceno; this.radioValue = res.Invoiceheader.invoicereferencetypeid; } });
现有问题
- 第一次点击绿色图标跳转时,HeaderInvoiceComponent的订阅触发2次,第1次返回null、第2次返回有效数据,添加非空判断后仅显示1次有效触发,请问如何优化为仅触发1次有效回调?
- 返回上一页后点击另一条目的绿色图标跳转,订阅会触发3次,请问是什么原因?
解决方案
问题1原因及优化方案
核心原因:BehaviorSubject的特性是创建时必须指定初始值,新订阅者接入时会优先推送当前存储的最新值,也就是你设置的初始值null,后续接口请求完成调用next推送新数据时才会触发第二次回调,所以才会出现两次触发的情况。
可选优化方案:
- 替换Subject类型:把BehaviorSubject换成ReplaySubject(1),ReplaySubject不需要初始值,只会推送调用next之后的历史数据,从根源上避免无效的初始值触发,修改SearchService代码即可:
// 替换原有BehaviorSubject声明 public setInvoiceData = new ReplaySubject<any>(1);
- 过滤无效回调:如果需要保留BehaviorSubject,就在订阅时添加RxJS的
skip(1)操作符,跳过第一次初始值推送即可:
// HeaderInvoiceComponent订阅逻辑修改 this.searchService.setInvoiceData .pipe(skip(1)) .subscribe(res => { // 原有业务逻辑不变 })
另外注意你现有代码的非空判断逻辑存在错误:res != null || res != undefined是或条件,无论res是null还是undefined都会满足条件进入逻辑,应该改成res != null && res != undefined,或者直接简化为if(res)即可。
问题2原因及修复方案
核心原因:你没有在HeaderInvoiceComponent销毁时手动取消订阅,每次进入页面创建新的组件实例时都会新增一个订阅,旧实例的订阅不会跟随组件销毁自动清除,所以跳转次数越多,累计的订阅数越多,触发回调的次数也会对应增加。
修复方案:在组件中存储订阅引用,组件销毁时手动取消订阅即可,修改HeaderInvoiceComponent代码:
// 新增变量存储订阅实例 private invoiceSub: Subscription; constructor(private searchService: SearchService) { // 订阅时赋值给变量 this.invoiceSub = this.searchService.setInvoiceData.subscribe(res => { // 原有业务逻辑不变 }) } // 新增销毁生命周期钩子取消订阅 ngOnDestroy() { if(this.invoiceSub) { this.invoiceSub.unsubscribe(); } }
如果使用Angular16及以上版本,也可以直接用takeUntilDestroyed操作符简化写法,无需手动处理取消逻辑。
内容的提问来源于stack exchange,提问作者Nok Pollawat
相关产品推荐
相关产品推荐

