RxJS 实战:使用 throttleTime 实现弹幕系统
概述
弹幕(Danmaku)是一种在视频或直播中实时显示用户评论的功能。在实现弹幕系统时,我们需要处理:
- 点击节流:用户快速点击时,限制弹幕创建频率
- 动画管理:管理弹幕的创建、动画和销毁
- 位置随机:弹幕在随机位置出现
- 性能优化:避免创建过多弹幕导致性能问题
本章将介绍如何使用 RxJS 的 throttleTime 操作符实现弹幕系统,并处理点击事件的节流。
弹幕系统的基本需求
- 输入框发送:用户输入文字后发送弹幕
- 点击触发:用户点击区域时创建随机弹幕
- 动画效果:弹幕从右到左移动
- 自动清理:弹幕动画结束后自动移除
实现思路
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 在弹幕动画结束后自动移除,避免内存泄漏。
执行流程示例
假设用户快速点击弹幕区域:
- 0ms:用户点击 →
clickSubject$发出事件 - 0ms:
throttleTime立即处理 → 创建弹幕 A - 100ms:用户再次点击 →
clickSubject$发出事件 - 100ms:
throttleTime忽略(在 300ms 内) - 200ms:用户再次点击 →
clickSubject$发出事件 - 200ms:
throttleTime忽略(在 300ms 内) - 400ms:用户再次点击 →
clickSubject$发出事件 - 400ms:
throttleTime处理(已超过 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 => {
// 输入框变化处理
});
注意事项
- 内存泄漏:确保弹幕动画结束后及时移除
- 性能问题:限制同时显示的弹幕数量
- 用户体验:合理设置节流时间,既限制频率又保持响应
- 动画流畅:使用 CSS 动画和
will-change优化性能
总结
使用 throttleTime 实现弹幕系统是一个优雅的解决方案,它通过限制点击事件的触发频率来确保:
- 性能优化:避免创建过多弹幕导致性能问题
- 用户体验:保持即时反馈,但限制频率
- 代码简洁:使用 RxJS 操作符,代码清晰易读
- 易于扩展:可以轻松添加更多功能(如弹幕过滤、弹幕样式等)
通过合理使用 RxJS 操作符(throttleTime、takeUntil 等),我们可以构建一个流畅、高效的弹幕系统。
记住:节流适合需要即时反馈但需要限制频率的场景,而防抖适合等待用户完成操作的场景。