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 += `
${text}
`; - item.innerHTML = html; + textEl.innerText = text; - // 样式与动画 + // 计算轨道 const tracks = 10; const trackIndex = Math.floor(Math.random() * tracks); const top = 5 + (trackIndex * (90 / tracks)); + el.style.top = `${top}%`; - item.style.top = `${top}%`; - - const duration = 18 + Math.random() * 7; - item.style.animation = `barrage-slide-right ${duration}s linear forwards`; + // 显示 + el.style.display = 'inline-flex'; - // 仅绑定必要的生命周期事件 - item.addEventListener('animationend', () => { - item.remove(); - this.activeIds.delete(id); + // 使用 Web Animations API (WAAPI) 替代 CSS Animation + // 优势:无需触发 offsetHeight 重绘,控制更精准 + const duration = (18 + Math.random() * 7) * 1000; // ms + + const keyframes = [ + { transform: 'translateX(-100%)', opacity: 0, offset: 0 }, + { opacity: 1, offset: 0.05 }, + { opacity: 1, offset: 0.95 }, + { transform: 'translateX(100vw)', opacity: 0, offset: 1 } + ]; + + const animation = el.animate(keyframes, { + duration: duration, + easing: 'linear', + fill: 'forwards' }); - this.container.appendChild(item); + animation.onfinish = () => { + this.recycle(el); + }; + + poolItem.animation = animation; } }