You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 08:30:02