From e66ecb0c809a5c3b223cad99ba8f4a19c6731d9b Mon Sep 17 00:00:00 2001
From: Vaica <94612053+zqlit@users.noreply.github.com>
Date: Tue, 3 Feb 2026 13:18:03 +0800
Subject: [PATCH] Update barrage.js
---
themes/Ying/assets/js/modules/barrage.js | 161 ++++++++++++++++++-----
1 file changed, 130 insertions(+), 31 deletions(-)
diff --git a/themes/Ying/assets/js/modules/barrage.js b/themes/Ying/assets/js/modules/barrage.js
index 04a72247..d0771e47 100644
--- a/themes/Ying/assets/js/modules/barrage.js
+++ b/themes/Ying/assets/js/modules/barrage.js
@@ -153,13 +153,69 @@
// 追踪当前屏幕上显示的弹幕 ID
this.activeIds = new Set();
- // 最大同屏数量限制,防止卡顿
+ // 最大同屏数量限制
this.maxActiveItems = 30;
+ // 对象池
+ this.pool = [];
+ this.initPool();
+
// 绑定事件委托
this.initEventDelegation();
}
+ initPool() {
+ // 初始化对象池,预创建 DOM
+ const fragment = document.createDocumentFragment();
+ for (let i = 0; i < this.maxActiveItems; i++) {
+ const item = document.createElement('div');
+ item.className = 'barrage-item';
+ item.style.display = 'none'; // 初始隐藏
+ item.style.willChange = 'transform'; // 提示浏览器优化
+
+ // 预构建结构
+ const avatar = document.createElement('img');
+ avatar.className = 'barrage-avatar';
+ avatar.draggable = false;
+
+ const bubble = document.createElement('div');
+ bubble.className = 'barrage-bubble';
+ const textSpan = document.createElement('span');
+ textSpan.className = 'barrage-text';
+ bubble.appendChild(textSpan);
+
+ item.appendChild(avatar);
+ item.appendChild(bubble);
+
+ // 绑定动画结束事件(只需绑定一次)
+ item.addEventListener('animationend', () => {
+ this.recycle(item);
+ });
+
+ this.pool.push({
+ el: item,
+ avatarEl: avatar,
+ textEl: textSpan,
+ busy: false,
+ id: null
+ });
+ fragment.appendChild(item);
+ }
+ this.container.appendChild(fragment);
+ }
+
+ recycle(itemEl) {
+ const poolItem = this.pool.find(p => p.el === itemEl);
+ if (poolItem) {
+ poolItem.busy = false;
+ poolItem.el.style.display = 'none';
+ if (poolItem.id) {
+ this.activeIds.delete(poolItem.id);
+ poolItem.id = null;
+ }
+ }
+ }
+
initEventDelegation() {
// 鼠标进入容器暂停
this.container.addEventListener('mouseover', (e) => {
@@ -205,10 +261,24 @@
start(data = []) {
this.stop();
- this.clearDOM();
+ // 不再清除 DOM,而是重置池子
+ this.resetPool();
this.isDestroyed = false;
this.updateData(data);
}
+
+ resetPool() {
+ this.pool.forEach(item => {
+ item.busy = false;
+ item.el.style.display = 'none';
+ item.id = null;
+ if (item.animation) {
+ item.animation.cancel();
+ item.animation = null;
+ }
+ });
+ this.activeIds.clear();
+ }
updateData(data = []) {
// 数据预处理:去重 & ID 规范化
@@ -258,20 +328,21 @@
clearTimeout(this.timer);
this.timer = null;
}
- // 注意:不清除 activeIds,允许已发射的弹幕飞完
}
clearDOM() {
- if (this.container) {
- this.container.innerHTML = '';
- }
- this.activeIds.clear();
+ // 兼容旧接口:实际上就是 resetPool
+ this.resetPool();
}
destroy() {
this.isDestroyed = true;
this.stop();
- this.clearDOM();
+ this.resetPool();
+ // 彻底销毁时才移除 DOM
+ if (this.container) {
+ this.container.innerHTML = '';
+ }
}
loop() {
@@ -316,42 +387,70 @@
shoot(itemData) {
const { text, avatar, url, id } = itemData;
+ // 查找空闲的池对象
+ const poolItem = this.pool.find(p => !p.busy);
+ if (!poolItem) return; // 没有空闲对象,放弃本次发射
+
+ poolItem.busy = true;
+ poolItem.id = id;
this.activeIds.add(id);
- const item = document.createElement('div');
- item.className = 'barrage-item';
- // 存储 URL 到 dataset 以供事件委托使用
+ const { el, avatarEl, textEl } = poolItem;
+
+ // 更新数据
if (url && url !== '#') {
- item.dataset.url = url;
- item.style.cursor = 'pointer';
+ el.dataset.url = url;
+ el.style.cursor = 'pointer';
+ } else {
+ el.removeAttribute('data-url');
+ el.style.cursor = 'default';
+ }
+
+ // 只有当有头像时才显示 img
+ if (avatar) {
+ avatarEl.src = avatar;
+ avatarEl.style.display = 'block';
+ // 错误处理
+ avatarEl.onerror = function() {
+ this.src = 'https://cravatar.cn/avatar/default?d=mp';
+ };
+ } else {
+ avatarEl.style.display = 'none';
}
- let html = '';
- // 优化:只有当有头像时才渲染 img 标签
- // 移除 loading="lazy" 以修复弹幕头像加载问题
- if (avatar) {
- html += ``;
- }
- html += `