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

如何确定触发Angular Effect执行的具体Signal?

在Angular Effect中如何判断是哪个Signal触发了执行?

我在组件中定义了两个Signal和一个Effect:当isLoggedIn Signal变化时,希望启动服务内的定时器函数;另一个idleTimeoutSignalModel Signal会基于服务定时器的状态变化频繁更新。

目前我通过在服务中设置isTimerRunning布尔值来避免重复执行定时器启动逻辑,但如果能直接判断触发Effect的Signal会更便捷。

我的代码

IdleTimeoutService 服务

import { Injectable, Signal, WritableSignal, signal } from '@angular/core';
import { Observable, Subscription, merge, fromEvent, timer, BehaviorSubject } from 'rxjs';
import { environment } from '../../environments/environment';
import { LocalStorageService } from './storage.service';

export interface IIdleTimeoutModel {
  expired: boolean;
  expiring: boolean;
}

@Injectable({
  providedIn: 'root',
})
export class IdleTimeoutService {
  constructor(private localStorageService: LocalStorageService) { }

  idleTimeout: IIdleTimeoutModel = { expired: false, expiring: false }
  private idleSignal: WritableSignal<IIdleTimeoutModel> = signal(this.idleTimeout);
  readonly idleSignalModel: Signal<IIdleTimeoutModel> = this.idleSignal.asReadonly();
  
  private idleEventTriggers: Observable<any> = new Observable();
  private timer: Subscription = new Subscription();
  private timeOutMilliSeconds: number = 1000;
  private timeOutWarningMilliSeconds: number = 1000;
  private isTimerRunning: boolean = false;

  public startWatching(): void {
    if (!this.isTimerRunning) {
      this.timeOutMilliSeconds = environment.idleTimeInMilliseconds;
      this.timeOutWarningMilliSeconds = environment.idleTimeWarningInMilliseconds;
  
      this.idleEventTriggers = merge(
        fromEvent(document, 'click'),
        fromEvent(document, 'mousedown'),
        fromEvent(document, 'keypress')
      );
  
      this.idleEventTriggers.subscribe((res) => {
        this.resetTimer();
      });
  
      this.startTimer();
     }
  }

  private startTimer() {
    this.isTimerRunning = true; // 当前用来避免重复启动定时器的临时方案
    let timeoutModel: IIdleTimeoutModel = {
      expired: false,
      expiring: false,
    };
    let initDatetime = new Date();
    let timeoutSeconds = this.timeOutWarningMilliSeconds / 1000 + initDatetime.getSeconds();
    let warningSeconds = this.timeOutMilliSeconds / 1000 + initDatetime.getSeconds();
    let expiringDatetime = new Date().setSeconds(timeoutSeconds);
    let expiredDatetime = new Date().setSeconds(warningSeconds);

    this.localStorageService.set('expiringDatetime', expiringDatetime.toString());
    this.localStorageService.set('expiredDatetime', expiredDatetime.toString());

    // 定时器逻辑
    this.timer = timer(1000, 5000).subscribe((response) => {
      let nowDatetime = new Date();

      // 检查即将过期时间
      let checkExpiringDatetimeValue = this.localStorageService.get('expiringDatetime');
      if (checkExpiringDatetimeValue) {
        if (nowDatetime.getTime() >= parseInt(checkExpiringDatetimeValue)) {
          this.idleSignal.set({ expiring: true, expired: false })
        } else {
          this.idleSignal.set({ expiring: false, expired: false })
        }
      } else {
        this.resetTimer();
      }

      // 检查已过期时间
      let checkExpiredDatetimeValue = this.localStorageService.get('expiredDatetime');
      if (checkExpiredDatetimeValue) {
        if (nowDatetime.getTime() >= parseInt(checkExpiredDatetimeValue)) {
          if (timeoutModel.expired === false) {
            timeoutModel.expired = true;
            this.idleSignal.set({ expiring: false, expired: true })
          }
        } else {
          if (timeoutModel.expired === true) {
            timeoutModel.expired = false;
            this.idleSignal.set({ expiring: false, expired: true })
          }
        }
      } else {
        this.resetTimer();
      }
    });
  }

  public resetTimer() {
    this.timer.unsubscribe();
    this.idleSignal.set({ expiring: false, expired: false })
    this.startTimer();
  }

  public stopTimer() {
    this.isTimerRunning = false;
    this.timer.unsubscribe();
  }
}

组件代码(AppComponent)

// 组件中声明的变量
isLoggedIn = this.appService.isLoggedInSignal; // 用户登录时会更新的Signal
private idleTimeoutSignalModel = this.idleTimeoutService.idleSignalModel; // 随定时器状态变化的Signal

// 构造函数中的Effect
effect(() => {
  if (this.isLoggedIn() && environment.idleTimeout) {
    const idleModel = this.idleTimeoutSignalModel();
    
    this.idleTimeoutService.startWatching(); // 希望只在isLoggedIn变化时执行此函数
    
    if (idleModel.expiring) {
      this.dialogService.openTimeOutDialog('延长会话?', '由于长时间未操作,您的登录会话即将过期。是否继续?');
    } else {
      this.dialogService.closeDialogById('timeout-dialog');
    }

    if (idleModel.expired) {
      try {
        this.broadcastService.publish({
          type: 'mnsso-logout',
          payload: 'true',
        });
      } catch {
        console.log('广播错误');
      }
      this._authService.logout();
    }
  }
});

解决方案

方案1:拆分Effect(推荐)

最清晰的做法是把两个独立的逻辑拆分成两个Effect,让每个Effect只响应自己关心的Signal,完全避免判断触发源的需求:

// 处理登录状态变化的Effect:仅在isLoggedIn变化时执行
effect(() => {
  if (environment.idleTimeout) {
    if (this.isLoggedIn()) {
      this.idleTimeoutService.startWatching();
    } else {
      // 用户登出时停止定时器
      this.idleTimeoutService.stopTimer();
    }
  }
});

// 处理超时状态的Effect:仅在idleTimeoutSignalModel变化时执行
effect(() => {
  if (!this.isLoggedIn() || !environment.idleTimeout) return;
  
  const idleModel = this.idleTimeoutSignalModel();
  
  if (idleModel.expiring) {
    this.dialogService.openTimeOutDialog('延长会话?', '由于长时间未操作,您的登录会话即将过期。是否继续?');
  } else {
    this.dialogService.closeDialogById('timeout-dialog');
  }

  if (idleModel.expired) {
    try {
      this.broadcastService.publish({
        type: 'mnsso-logout',
        payload: 'true',
      });
    } catch {
      console.log('广播错误');
    }
    this._authService.logout();
  }
});

这种方式符合单一职责原则,代码逻辑更清晰,也不会出现重复执行startWatching的问题。

方案2:在单个Effect中跟踪Signal变化

如果一定要在同一个Effect中处理,可以通过保存Signal的前值来判断是否是isLoggedIn变化触发了Effect:

private previousLoggedIn = this.isLoggedIn();

effect(() => {
  const currentLoggedIn = this.isLoggedIn();
  const idleModel = this.idleTimeoutSignalModel();
  
  if (environment.idleTimeout) {
    // 判断是否是isLoggedIn的变化触发了当前Effect
    if (currentLoggedIn !== this.previousLoggedIn) {
      this.previousLoggedIn = currentLoggedIn;
      if (currentLoggedIn) {
        this.idleTimeoutService.startWatching();
      } else {
        this.idleTimeoutService.stopTimer();
      }
    }
    
    // 处理超时状态逻辑
    if (currentLoggedIn) {
      if (idleModel.expiring) {
        this.dialogService.openTimeOutDialog('延长会话?', '由于长时间未操作,您的登录会话即将过期。是否继续?');
      } else {
        this.dialogService.closeDialogById('timeout-dialog');
      }

      if (idleModel.expired) {
        try {
          this.broadcastService.publish({
            type: 'mnsso-logout',
            payload: 'true',
          });
        } catch {
          console.log('广播错误');
        }
        this._authService.logout();
      }
    }
  }
});

这种方式不需要修改服务端的isTimerRunning逻辑,但代码复杂度比拆分Effect高,仅适合特殊场景下使用。


内容的提问来源于stack exchange,提问作者steveareeno

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 12:30:57