20个例子掌握RxJS——第十二章使用 throttleTime 实现弹幕系统

25 阅读3分钟

RxJS 实战:使用 throttleTime 实现弹幕系统

概述

弹幕(Danmaku)是一种在视频或直播中实时显示用户评论的功能。在实现弹幕系统时,我们需要处理:

  1. 点击节流:用户快速点击时,限制弹幕创建频率
  2. 动画管理:管理弹幕的创建、动画和销毁
  3. 位置随机:弹幕在随机位置出现
  4. 性能优化:避免创建过多弹幕导致性能问题

本章将介绍如何使用 RxJS 的 throttleTime 操作符实现弹幕系统,并处理点击事件的节流。

弹幕系统的基本需求

  1. 输入框发送:用户输入文字后发送弹幕
  2. 点击触发:用户点击区域时创建随机弹幕
  3. 动画效果:弹幕从右到左移动
  4. 自动清理:弹幕动画结束后自动移除

实现思路

1. 弹幕数据结构

// 弹幕项接口
interface DanmakuItem {
  id: number;
  text: string;
  top: number; // 弹幕的垂直位置(百分比)
  color: string; // 弹幕颜色
  speed: number; // 弹幕速度(秒)
}

2. 点击节流

使用 throttleTime 限制点击事件的触发频率:

// 点击节流 Subject
private clickSubject$ = new Subject<MouseEvent>();

// 销毁 Subject
private destroySubject$ = new Subject<void>();

ngOnInit(): void {
  // 设置点击节流:每 300ms 最多触发一次
  this.clickSubject$
    .pipe(
      throttleTime(300), // 节流:每 300ms 最多触发一次
      takeUntil(this.destroySubject$)
    )
    .subscribe((event) => {
      this.createDanmakuFromClick(event);
    });
}

// 点击区域触发弹幕(带节流)
onDanmakuAreaClick(event: MouseEvent): void {
  this.clickSubject$.next(event);
}

3. 创建弹幕

// 弹幕颜色池
private readonly colors = [
  '#ffffff',
  '#ff6b6b',
  '#4ecdc4',
  '#45b7d1',
  '#f9ca24',
  '#6c5ce7',
  '#a29bfe',
  '#fd79a8',
  '#00b894',
  '#e17055',
];

// 创建弹幕
private createDanmaku(text: string): void {
  const danmaku: DanmakuItem = {
    id: ++this.danmakuIdCounter,
    text,
    top: Math.random() * 80 + 10, // 10% - 90% 之间的随机位置
    color: this.colors[Math.floor(Math.random() * this.colors.length)],
    speed: Math.random() * 3 + 5, // 5-8 秒之间随机速度
  };

  this.danmakuList.push(danmaku);
  this.cdr.detectChanges();

  // 弹幕动画结束后移除(速度 + 0.5秒缓冲)
  setTimeout(() => {
    const index = this.danmakuList.findIndex((item) => item.id === danmaku.id);
    if (index !== -1) {
      this.danmakuList.splice(index, 1);
      this.cdr.detectChanges();
    }
  }, (danmaku.speed + 0.5) * 1000);
}

// 从点击事件创建弹幕
private createDanmakuFromClick(event: MouseEvent): void {
  const clickTexts = [
    '666',
    '太棒了!',
    '厉害!',
    '赞!',
    '好!',
    '不错!',
    '支持!',
    '加油!',
    '很棒!',
    '优秀!',
  ];
  const randomText = clickTexts[Math.floor(Math.random() * clickTexts.length)];
  this.createDanmaku(randomText);
}

4. 输入框发送

// 弹幕输入文字
danmakuText = '';

// 发送弹幕(从输入框)
sendDanmaku(): void {
  if (!this.danmakuText.trim()) {
    return;
  }

  this.createDanmaku(this.danmakuText.trim());
  this.danmakuText = ''; // 清空输入框
}

// 回车键发送弹幕
onKeyDown(event: KeyboardEvent): void {
  if (event.key === 'Enter' && !event.shiftKey) {
    event.preventDefault();
    this.sendDanmaku();
  }
}

CSS 动画实现

弹幕的移动动画通过 CSS 实现:

.danmaku-item {
  position: absolute;
  white-space: nowrap;
  font-size: 20px;
  font-weight: bold;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
  animation: danmaku-move linear;
  pointer-events: none;
  z-index: 10;
}

@keyframes danmaku-move {
  from {
    left: 100%;
    transform: translateX(0);
  }
  to {
    left: 0;
    transform: translateX(-100%);
  }
}

关键点解析

1. throttleTime 的作用

使用 throttleTime(300) 可以:

  • 限制点击事件的触发频率
  • 避免用户快速点击时创建过多弹幕
  • 提升性能和用户体验

2. 弹幕位置随机

通过 Math.random() * 80 + 10 生成 10% - 90% 之间的随机位置,避免弹幕重叠。

3. 弹幕速度随机

通过 Math.random() * 3 + 5 生成 5-8 秒之间的随机速度,让弹幕移动更自然。

4. 自动清理

使用 setTimeout 在弹幕动画结束后自动移除,避免内存泄漏。

执行流程示例

假设用户快速点击弹幕区域:

  1. 0ms:用户点击 → clickSubject$ 发出事件
  2. 0msthrottleTime 立即处理 → 创建弹幕 A
  3. 100ms:用户再次点击 → clickSubject$ 发出事件
  4. 100msthrottleTime 忽略(在 300ms 内)
  5. 200ms:用户再次点击 → clickSubject$ 发出事件
  6. 200msthrottleTime 忽略(在 300ms 内)
  7. 400ms:用户再次点击 → clickSubject$ 发出事件
  8. 400msthrottleTime 处理(已超过 300ms)→ 创建弹幕 B

结果:300ms 内只创建 1 个弹幕,避免过多弹幕。

与其他方案的对比

方案 1:不使用节流(有问题)

// ❌ 错误示例:快速点击会创建过多弹幕
onDanmakuAreaClick(event: MouseEvent): void {
  this.createDanmakuFromClick(event); // 每次点击都创建
}

方案 2:使用防抖(不适合)

// ⚠️ 不适合:防抖会等待用户停止点击,但弹幕需要即时反馈
onDanmakuAreaClick(event: MouseEvent): void {
  debounceTime(300).subscribe(() => {
    this.createDanmakuFromClick(event);
  });
}

方案 3:使用节流(推荐)✅

// ✅ 推荐:限制频率但保持即时反馈
this.clickSubject$.pipe(
  throttleTime(300)
).subscribe(event => {
  this.createDanmakuFromClick(event);
});

实际应用场景

1. 视频弹幕

// 视频播放时显示弹幕
playVideo().pipe(
  switchMap(() => 
    this.danmakuService.getDanmakus(videoId).pipe(
      mergeMap(danmaku => {
        // 根据视频时间显示弹幕
        return timer(danmaku.time * 1000).pipe(
          map(() => danmaku)
        );
      })
    )
  )
).subscribe(danmaku => {
  this.createDanmaku(danmaku.text);
});

2. 直播弹幕

// 接收直播弹幕
this.websocketService.onMessage('danmaku').pipe(
  throttleTime(100) // 限制弹幕创建频率
).subscribe(danmaku => {
  this.createDanmaku(danmaku.text);
});

3. 互动游戏

// 游戏中的弹幕效果
onPlayerAction(action: string): void {
  this.actionSubject$.next(action);
}

this.actionSubject$.pipe(
  throttleTime(500) // 限制动作触发频率
).subscribe(action => {
  this.createDanmaku(action);
});

性能优化建议

1. 限制弹幕数量

限制同时显示的弹幕数量,避免性能问题:

// 限制弹幕数量
private readonly MAX_DANMAKU = 50;

private createDanmaku(text: string): void {
  // 如果弹幕数量超过限制,移除最旧的
  if (this.danmakuList.length >= this.MAX_DANMAKU) {
    this.danmakuList.shift();
  }
  
  // 创建新弹幕
  // ...
}

2. 使用虚拟滚动

对于大量弹幕,可以使用虚拟滚动技术:

// 只渲染可见区域的弹幕
getVisibleDanmakus(): DanmakuItem[] {
  return this.danmakuList.filter(danmaku => {
    // 判断弹幕是否在可见区域
    return this.isDanmakuVisible(danmaku);
  });
}

3. 使用 CSS 动画

使用 CSS 动画而不是 JavaScript 动画,性能更好:

.danmaku-item {
  animation: danmaku-move linear;
  will-change: transform; /* 优化性能 */
}

4. 防抖输入框

对于输入框发送,可以结合防抖:

this.danmakuInput.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged()
).subscribe(value => {
  // 输入框变化处理
});

注意事项

  1. 内存泄漏:确保弹幕动画结束后及时移除
  2. 性能问题:限制同时显示的弹幕数量
  3. 用户体验:合理设置节流时间,既限制频率又保持响应
  4. 动画流畅:使用 CSS 动画和 will-change 优化性能

总结

使用 throttleTime 实现弹幕系统是一个优雅的解决方案,它通过限制点击事件的触发频率来确保:

  • 性能优化:避免创建过多弹幕导致性能问题
  • 用户体验:保持即时反馈,但限制频率
  • 代码简洁:使用 RxJS 操作符,代码清晰易读
  • 易于扩展:可以轻松添加更多功能(如弹幕过滤、弹幕样式等)

通过合理使用 RxJS 操作符(throttleTimetakeUntil 等),我们可以构建一个流畅、高效的弹幕系统。

记住:节流适合需要即时反馈但需要限制频率的场景,而防抖适合等待用户完成操作的场景

码云地址:gitee.com/leeyamaster…