Update barrage.js

This commit is contained in:
Vaica committed 2026-02-03 13:18:03 +08:00
1 parent 761d9024e7
commit ea6125853e
1 file changed
+130 -31
+130 -31
View File
@@ -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 += `<img src="${avatar}" class="barrage-avatar" draggable="false" onerror="this.src='https://cravatar.cn/avatar/default?d=mp'">`;
}
html += `<div class="barrage-bubble"><span class="barrage-text">${text}</span></div>`;
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;
}
}