Update barrage.js
This commit is contained in:
1 parent
761d9024e7
commit
ea6125853e
1 file changed
+130
-31
@@ -153,13 +153,69 @@
|
|||||||
|
|
||||||
// 追踪当前屏幕上显示的弹幕 ID
|
// 追踪当前屏幕上显示的弹幕 ID
|
||||||
this.activeIds = new Set();
|
this.activeIds = new Set();
|
||||||
// 最大同屏数量限制,防止卡顿
|
// 最大同屏数量限制
|
||||||
this.maxActiveItems = 30;
|
this.maxActiveItems = 30;
|
||||||
|
|
||||||
|
// 对象池
|
||||||
|
this.pool = [];
|
||||||
|
this.initPool();
|
||||||
|
|
||||||
// 绑定事件委托
|
// 绑定事件委托
|
||||||
this.initEventDelegation();
|
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() {
|
initEventDelegation() {
|
||||||
// 鼠标进入容器暂停
|
// 鼠标进入容器暂停
|
||||||
this.container.addEventListener('mouseover', (e) => {
|
this.container.addEventListener('mouseover', (e) => {
|
||||||
@@ -205,10 +261,24 @@
|
|||||||
|
|
||||||
start(data = []) {
|
start(data = []) {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.clearDOM();
|
// 不再清除 DOM,而是重置池子
|
||||||
|
this.resetPool();
|
||||||
this.isDestroyed = false;
|
this.isDestroyed = false;
|
||||||
this.updateData(data);
|
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 = []) {
|
updateData(data = []) {
|
||||||
// 数据预处理:去重 & ID 规范化
|
// 数据预处理:去重 & ID 规范化
|
||||||
@@ -258,20 +328,21 @@
|
|||||||
clearTimeout(this.timer);
|
clearTimeout(this.timer);
|
||||||
this.timer = null;
|
this.timer = null;
|
||||||
}
|
}
|
||||||
// 注意:不清除 activeIds,允许已发射的弹幕飞完
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDOM() {
|
clearDOM() {
|
||||||
if (this.container) {
|
// 兼容旧接口:实际上就是 resetPool
|
||||||
this.container.innerHTML = '';
|
this.resetPool();
|
||||||
}
|
|
||||||
this.activeIds.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.isDestroyed = true;
|
this.isDestroyed = true;
|
||||||
this.stop();
|
this.stop();
|
||||||
this.clearDOM();
|
this.resetPool();
|
||||||
|
// 彻底销毁时才移除 DOM
|
||||||
|
if (this.container) {
|
||||||
|
this.container.innerHTML = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loop() {
|
loop() {
|
||||||
@@ -316,42 +387,70 @@
|
|||||||
shoot(itemData) {
|
shoot(itemData) {
|
||||||
const { text, avatar, url, id } = 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);
|
this.activeIds.add(id);
|
||||||
|
|
||||||
const item = document.createElement('div');
|
const { el, avatarEl, textEl } = poolItem;
|
||||||
item.className = 'barrage-item';
|
|
||||||
// 存储 URL 到 dataset 以供事件委托使用
|
// 更新数据
|
||||||
if (url && url !== '#') {
|
if (url && url !== '#') {
|
||||||
item.dataset.url = url;
|
el.dataset.url = url;
|
||||||
item.style.cursor = 'pointer';
|
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 = '';
|
textEl.innerText = text;
|
||||||
// 优化:只有当有头像时才渲染 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;
|
|
||||||
|
|
||||||
// 样式与动画
|
// 计算轨道
|
||||||
const tracks = 10;
|
const tracks = 10;
|
||||||
const trackIndex = Math.floor(Math.random() * tracks);
|
const trackIndex = Math.floor(Math.random() * tracks);
|
||||||
const top = 5 + (trackIndex * (90 / tracks));
|
const top = 5 + (trackIndex * (90 / tracks));
|
||||||
|
el.style.top = `${top}%`;
|
||||||
|
|
||||||
item.style.top = `${top}%`;
|
// 显示
|
||||||
|
el.style.display = 'inline-flex';
|
||||||
const duration = 18 + Math.random() * 7;
|
|
||||||
item.style.animation = `barrage-slide-right ${duration}s linear forwards`;
|
|
||||||
|
|
||||||
// 仅绑定必要的生命周期事件
|
// 使用 Web Animations API (WAAPI) 替代 CSS Animation
|
||||||
item.addEventListener('animationend', () => {
|
// 优势:无需触发 offsetHeight 重绘,控制更精准
|
||||||
item.remove();
|
const duration = (18 + Math.random() * 7) * 1000; // ms
|
||||||
this.activeIds.delete(id);
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user