AngularFire操作Firebase删除子节点触发栈溢出错误求助
问题分析与解决方案
背景信息
使用Angular结合AngularFire连接Firebase实时数据库,数据库结构如下:
-requests |-openRequests |-{RequestId} |-... |-acceptedRequests |-{RequestId} |-...
需求为用户接受请求时,将openRequests中的条目迁移至acceptedRequests,当前实现代码如下:
import { Injectable } from '@angular/core'; import { AngularFireDatabase } from '@angular/fire/compat/database'; import { AuthService } from '../../shared/services/auth.service'; import { Subscription, take } from 'rxjs'; import { Router } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class RequestsService { newRequestAvailable: boolean = true; openRequestsSubscription: Subscription | undefined; constructor( private rtdb: AngularFireDatabase, private authService: AuthService, private router: Router ) { this.subscribeToOpenRequests(); } acceptRequest() { //this.unsubscribeFromOpenRequests(); this.rtdb .list('/requests/openRequests') .valueChanges() .pipe(take(1)) .subscribe((data) => { if (data.length === 0) { console.log('no open requests'); return; } const request = data.reduce((prev: any, current: any) => prev.timestamp < current.timestamp ? prev : current ); request.status = 'accepted'; request.stylistUId = this.authService.userData.uid; request.stylistName = this.authService.userData.displayName; this.rtdb .list('/requests/openRequests') .remove(request.userUId) .then(() => { this.rtdb.list('/requests/acceptedRequests').push(request); }) .catch((error) => { console.error('Error while accepting request', error); }); }); this.router.navigate(['answer']); } subscribeToOpenRequests() { this.openRequestsSubscription = this.rtdb .list('/requests/openRequests') .valueChanges() .subscribe((data) => { //Error still persists even when I am doing nothing in here /*if (data.length > 0) { this.newRequestAvailable = true; } else { this.newRequestAvailable = false; }*/ }); } unsubscribeFromOpenRequests() { if (this.openRequestsSubscription) { this.openRequestsSubscription.unsubscribe(); } } }
遇到的问题
调用acceptRequest函数时频繁触发Maximum call stack size exceeded错误,确认由无限递归循环导致,但无法定位具体位置。调用unsubscribeFromOpenRequests()可暂时解决,但新增条目至openRequests时错误复现。
问题排查与优化方案
1. 递归循环根源
- 核心问题在于
valueChanges()订阅与数据库操作的联动:当你在acceptRequest中删除openRequests下的条目时,subscribeToOpenRequests中的订阅会触发回调;即使当前回调逻辑被注释,重复订阅或异常重试也可能引发栈溢出。 - 另外,
valueChanges()仅返回数据数组,不包含节点key,你用request.userUId作为移除的键值,会导致移除操作精准度不足,触发异常重试,间接加剧循环问题。
2. 优化实现方案
方案一:使用snapshotChanges()精准操作节点
snapshotChanges()会返回包含节点key和数据的对象,能确保操作精准,同时优化订阅逻辑避免循环:
import { Injectable } from '@angular/core'; import { AngularFireDatabase } from '@angular/fire/compat/database'; import { AuthService } from '../../shared/services/auth.service'; import { Subscription, take } from 'rxjs'; import { Router } from '@angular/router'; import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class RequestsService { newRequestAvailable: boolean = true; openRequestsSubscription: Subscription | undefined; constructor( private rtdb: AngularFireDatabase, private authService: AuthService, private router: Router ) { this.subscribeToOpenRequests(); } acceptRequest() { this.rtdb .list('/requests/openRequests') .snapshotChanges() .pipe( take(1), map(actions => actions.map(action => ({ key: action.key, ...action.payload.val() })) ) ) .subscribe((requests) => { if (requests.length === 0) { console.log('no open requests'); return; } // 筛选最早的请求 const earliestRequest = requests.reduce((prev, current) => prev.timestamp < current.timestamp ? prev : current ); const updatedRequest = { ...earliestRequest, status: 'accepted', stylistUId: this.authService.userData.uid, stylistName: this.authService.userData.displayName }; // 用节点key精准移除 this.rtdb.object(`/requests/openRequests/${earliestRequest.key}`) .remove() .then(() => { this.rtdb.list('/requests/acceptedRequests').push(updatedRequest); this.router.navigate(['answer']); }) .catch((error) => { console.error('Error while accepting request', error); }); }); } subscribeToOpenRequests() { // 先取消已有订阅,避免重复订阅引发的多次回调 this.unsubscribeFromOpenRequests(); this.openRequestsSubscription = this.rtdb .list('/requests/openRequests') .valueChanges() .subscribe((data) => { this.newRequestAvailable = data.length > 0; }); } unsubscribeFromOpenRequests() { if (this.openRequestsSubscription) { this.openRequestsSubscription.unsubscribe(); this.openRequestsSubscription = undefined; } } // 组件销毁时调用,防止内存泄漏 ngOnDestroy() { this.unsubscribeFromOpenRequests(); } }
方案二:使用Firebase事务保证原子性(推荐)
针对并发场景,用事务确保移除和添加操作的原子性,避免数据不一致:
acceptRequest() { const openRef = this.rtdb.database.ref('/requests/openRequests'); openRef.orderByChild('timestamp').limitToFirst(1).once('value') .then(snapshot => { if (!snapshot.exists()) { console.log('no open requests'); return; } const requestKey = Object.keys(snapshot.val())[0]; const requestData = snapshot.val()[requestKey]; const updatedRequest = { ...requestData, status: 'accepted', stylistUId: this.authService.userData.uid, stylistName: this.authService.userData.displayName }; // 原子操作:移除open中的条目并添加到accepted return this.rtdb.database.ref().transaction(ref => { if (ref && ref.requests && ref.requests.openRequests && ref.requests.openRequests[requestKey]) { // 删除open中的请求 delete ref.requests.openRequests[requestKey]; // 添加到accepted if (!ref.requests.acceptedRequests) { ref.requests.acceptedRequests = {}; } ref.requests.acceptedRequests[requestKey] = updatedRequest; } return ref; }); }) .then(() => { this.router.navigate(['answer']); }) .catch(error => { console.error('Error while accepting request', error); }); }
3. 关键优化点
- 替换
valueChanges()为snapshotChanges()获取节点key,确保移除操作精准无误。 - 在
subscribeToOpenRequests中先取消已有订阅,避免重复订阅导致的多次回调。 - 将
router.navigate放在异步操作的then回调中,确保数据操作完成后再跳转。 - 使用事务保证数据操作的原子性,避免并发场景下的数据不一致问题。
内容的提问来源于stack exchange,提问作者Julius
相关产品推荐
相关产品推荐

