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

Ionic/Angular中如何重置超时器与动画?

问题描述

预期通知展示行为

  • 点击按钮后,通知滑下显示文本
  • 通知自动滑回;若有多条消息,每2秒切换显示下一条

重复点击的期望行为

  • 点击按钮,通知滑下显示文本
  • 再次点击时,立即停止当前动画,重置计时器与消息队列
  • 显示新消息并重新启动计时器,最后通知滑回

遇到的错误

  • [ng] 11: Cannot find namespace 'NodeJS',对应代码行:endAnimationTimeout: NodeJS.Timeout | undefined;
  • [ng] 10: Cannot find namespace 'NodeJS',对应代码行:startAnimationTimeout: NodeJS.Timeout | undefined;

注意:消息组件被其他组件调用


相关代码

消息组件TS代码

import { Component, OnInit } from '@angular/core';
import { ValidatorsService } from '../../../services/validators.service';

@Component({
  selector: 'app-message',
  templateUrl: './message.component.html',
  styleUrls: ['./message.component.scss'],
})
export class MessageComponent implements OnInit {
  startAnimationTimeout: NodeJS.Timeout | undefined;
  endAnimationTimeout: NodeJS.Timeout | undefined;

  constructor(public validatorsService: ValidatorsService) { }

  ngOnInit() {
    let card = document.querySelector('.message-card') as HTMLDivElement;
    let timeBar = document.querySelector('.time-bar') as HTMLDivElement;
    this.showMessage(card, timeBar);
  }

  messageAnimation(card: HTMLDivElement, timeBar: HTMLDivElement) {
    if (this.validatorsService.messageIndex <= this.validatorsService.messages.length - 1) {
      timeBar.classList.remove('slideToLeft')
    }

    clearTimeout(this.startAnimationTimeout);
    this.startAnimationTimeout = setTimeout(() => {
      timeBar.classList.add('slideToLeft');

      this.endAnimationTimeout = setTimeout(() => {
        if (this.validatorsService.messageIndex >= this.validatorsService.messages.length - 1) {
          card.classList.replace('slideDown', 'slideUp');

          setTimeout(() => {
            card.classList.replace('slideUp', 'hide');
            timeBar.classList.remove('slideToLeft')
          }, 750);
        }
      }, 2000);
    }, 500);
  }

  showMessage(card: HTMLDivElement, timeBar: HTMLDivElement) {
    if (this.validatorsService.messages.length === 1) {
      clearTimeout(this.startAnimationTimeout);
      clearTimeout(this.endAnimationTimeout);
      this.messageAnimation(card, timeBar);
      return;
    }

    if (this.validatorsService.messageIndex >= this.validatorsService.messages.length) {
      clearTimeout(this.startAnimationTimeout);
      return;
    }

    this.messageAnimation(card, timeBar);

    setTimeout(() => {
      this.validatorsService.messageIndex++;
      this.messageAnimation(card, timeBar);
      this.showMessage(card, timeBar);
    }, 2600);

    return;
  };
}

消息组件HTML代码

<div #card class="message-card slideDown">
  <p class="message"
    [ngClass]="{'validMessage': validatorsService.messageType === 'success', 'warningMessage': validatorsService.messageType === 'warning'}">
    <span *ngIf="validatorsService.messageType === 'error'">Error
      {{validatorsService.messageIndex + 1}} of {{validatorsService.messages.length}}:</span>
    {{validatorsService.messages[validatorsService.messageIndex]}}</p>

  <div #timeBar class="time-bar"
    [ngClass]="{'valid': validatorsService.messageType === 'success', 'warning': validatorsService.messageType === 'warning'}">
  </div>
</div>

ValidatorsService及按钮点击函数

import { Injectable } from '@angular/core';
import { AbstractControl } from '@angular/forms';

@Injectable({
  providedIn: 'root'
})
export class ValidatorsService {
  messages: string[] = [];
  messageIndex: number = 0;
  messageType: string = 'error';
  showMessage: boolean = false;

  constructor() { }

  // Form validations
  noSpaceAllowed(control: AbstractControl) {
    let parentControl: any = control.parent;
    let controlName;

    if (parentControl) {
      controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control);
    }

    if (control.value !== null && control.value.indexOf(' ') !== -1) {
      return { error: `Your ${controlName} can't contain spaces.` };
    }

    return null;
  }

  noEmptyAllowed(control: AbstractControl) {
    let parentControl: any = control.parent;
    let controlName;

    if (parentControl) {
      controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control);
    }

    if (control.value !== null && control.value === '') {
      return { error: `Your ${controlName} can't be empty.` };
    }

    return null;
  }

  lengthRangeAllowed(control: AbstractControl) {
    let parentControl: any = control.parent;
    let controlName;

    if (parentControl) {
      controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control);
    }

    if (controlName === 'email' && control.value !== null && control.value.length > 254) {
      return { error: `Your ${controlName} must have less then 254 characters.` };
    }

    if (controlName === 'username' && control.value !== null && control.value.length > 20) {
      return { error: `Your ${controlName} must have less then 20 characters.` };
    }

    if (controlName === 'password' && control.value !== null && control.value.length < 6) {
      return { error: `Your ${controlName} must contain a minimum of 6 characters.` };
    }

    if (controlName === 'password' && control.value !== null && control.value.length > 24) {
      return { error: `Your ${controlName} must have less then 24 characters.` };
    }

    return null;
  }

  noSpaceOnlyAllowed(control: AbstractControl) {
    let parentControl: any = control.parent;
    let controlName;

    if (parentControl) {
      controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control);
    }

    if (control.value !== null && control.value !== '' && /^\s*$/.test(control.value)) {
      return { error: `Your ${controlName} can't contain just spaces.` };
    }

    return null;
  }

  noConsecutiveSpacesAllowed(control: AbstractControl) {
    let parentControl: any = control.parent;
    let controlName;

    if (parentControl) {
      controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control);
    }

    if (control.value !== null && control.value !== '' && /\s{2,}/.test(control.value)) {
      return { error: `Your ${controlName} can't contain consecutive spaces.` };
    }

    return null;
  }

  noStartNorEndSpacesAllowed(control: AbstractControl) {
    let parentControl: any = control.parent;
    let controlName;

    if (parentControl) {
      controlName = Object.keys(parentControl.controls).find(key => parentControl.controls[key] === control);
    }

    if (control.value !== null && control.value !== '' && control.value.startsWith(" ")) {
      return { error: `Your ${controlName} can't start with a space.` };
    }

    if (control.value !== null && control.value !== '' && control.value.endsWith(" ")) {
      return { error: `Your ${controlName} can't end with a space.` };
    }

    return null;
  }

  acceptToSValidation(control: AbstractControl) {
    if (control.value !== null && control.value === false) { return { error: 'You Must Agree to the Terms & Conditions.' }; }

    return null;
  }
}
formValidation() {
    this.validatorsService.messages = [];
    this.validatorsService.messageIndex = 0;

    Object.keys(this.form.controls).forEach(key => {
      let control: any = this.form.controls[key];

      if (control.errors) {
        Object.keys(control.errors).forEach(error => {
          this.validatorsService.messages.push(control.errors[error]);
        });
      };
    });

    this.validatorsService.showMessage = true;
  }

  onSubmit() {
    this.validatorsService.showMessage = false

    Object.values(this.form.controls).forEach(control => {
      control.markAsTouched();
    });

    if (this.form.valid === false) {
      this.validatorsService.messageType = 'error';
      this.formValidation();
      return;
    }

    this.validatorsService.messageType = 'success';

    console.log(this.form.value);
  }

父组件引用消息组件的HTML

<app-message *ngIf="this.validatorsService.showMessage"></app-message>

解决方案

1. 修复NodeJS命名空间错误

Angular浏览器环境无需使用NodeJS.Timeout,直接用ReturnType<typeof setTimeout>替代(类型更安全):

startAnimationTimeout: ReturnType<typeof setTimeout> | undefined;
endAnimationTimeout: ReturnType<typeof setTimeout> | undefined;

若确实需要NodeJS类型支持,可安装依赖并配置:

npm install @types/node --save-dev

修改tsconfig.json:

{
  "compilerOptions": {
    "types": ["node"]
  }
}

2. 实现重复点击重置逻辑

步骤1:添加重置方法到消息组件

resetNotifications(card: HTMLDivElement, timeBar: HTMLDivElement) {
  // 清除所有计时器
  clearTimeout(this.startAnimationTimeout);
  clearTimeout(this.endAnimationTimeout);
  // 重置动画状态
  card.classList.remove('slideUp', 'hide');
  card.classList.add('slideDown');
  timeBar.classList.remove('slideToLeft');
  // 重置消息索引
  this.validatorsService.messageIndex = 0;
}

步骤2:监听消息数组变化

修改ValidatorsService,用BehaviorSubject实现消息数组的可观察性:

import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ValidatorsService {
  messages$ = new BehaviorSubject<string[]>([]);
  messageIndex: number = 0;
  messageType: string = 'error';
  showMessage: boolean = false;

  // ... 其他验证方法保持不变
}

更新formValidation方法:

formValidation() {
  const newMessages: string[] = [];
  Object.keys(this.form.controls).forEach(key => {
    let control: any = this.form.controls[key];
    if (control.errors) {
      Object.keys(control.errors).forEach(error => {
        newMessages.push(control.errors[error]);
      });
    };
  });
  this.validatorsService.messages$.next(newMessages);
  this.validatorsService.showMessage = true;
}

在消息组件中订阅变化并重置:

import { Subscription } from 'rxjs';

export class MessageComponent implements OnInit, OnDestroy {
  startAnimationTimeout: ReturnType<typeof setTimeout> | undefined;
  endAnimationTimeout: ReturnType<typeof setTimeout> | undefined;
  private messagesSubscription!: Subscription;

  constructor(public validatorsService: ValidatorsService) { }

  ngOnInit() {
    const card = document.querySelector('.message-card') as HTMLDivElement;
    const timeBar = document.querySelector('.time-bar') as HTMLDivElement;

    this.messagesSubscription = this.validatorsService.messages$.subscribe(() => {
      this.resetNotifications(card, timeBar);
      this.showMessage(card, timeBar);
    });

    this.showMessage(card, timeBar);
  }

  ngOnDestroy() {
    this.messagesSubscription.unsubscribe();
    clearTimeout(this.startAnimationTimeout);
    clearTimeout(this.endAnimationTimeout);
  }

  // ... 其他方法保持不变
}

步骤3:优化消息展示逻辑

替换递归调用为可控的计时器链,避免内存泄漏:

showMessage(card: HTMLDivElement, timeBar: HTMLDivElement) {
  const messages = this.validatorsService.messages$.value;
  if (messages.length === 0) return;

  const displayNext = () => {
    if (this.validatorsService.messageIndex >= messages.length) {
      // 所有消息展示完毕,隐藏卡片
      card.classList.replace('slideDown', 'slideUp');
      setTimeout(() => {
        card.classList.replace('slideUp', 'hide');
        timeBar.classList.remove('slideToLeft');
      }, 750);
      return;
    }

    this.messageAnimation(card, timeBar);
    this.validatorsService.messageIndex++;
    setTimeout(displayNext, 2600);
  };

  displayNext();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 04:25:57