来, vue3实现一个高性能的弹幕组件

1,096 阅读5分钟

先有问题再有答案

  1. vue中如何获取dom的宽高属性?
  2. 100条数据就要设置100个dom嘛?
  3. 每个弹幕动画的运行效果 具体是如何实现的?
  4. 每个弹幕的长度是不固定的 如何正确获取长度值 给动画设置正确的结束位置?
  5. 如何利用raf实现高性能的动画效果?
  6. vue中的插槽究竟要如何使用?
  7. 插槽的本质是什么?

效果

danmu2.gif

组件功能

  1. 多行显示:row指定某条弹幕随机出现在第[1,N]行。
  2. 动态延迟:每条弹幕的显示延迟可以随机生成,使得弹幕的出现更加自然。
  3. 可复用实例:通过复用实例来优化性能,减少频繁创建和销毁 DOM 元素的开销。
  4. 自定义样式:可以通过插槽(slot)来自定义每个弹幕项的样式和内容 长度不限
  5. 响应式更新:当传入的数据列表发生变化时,组件会自动更新并重新安排弹幕的显示。

使用方式

<DanmuView :count="8" :row="4" :row-h="30" :delays="[500, 3000]" :frames="[0.5, 1.2]" :list-data="list">
                <template #default="{ data }">
                    <div class="item" @click="onItem(data)">
                        <img :src="data.icon" :width="18" class="icon" />
                        <div class="name">
                            {{ data.content }}
                        </div>
                    </div>
                </template>
            </DanmuView>
  • count:设置可复用的实例化数量。
  • row:设置弹幕轨道行数。
  • row-h:设置每行的高度。
  • delays:设置每条弹幕的随机延迟范围。
  • frames:设置每帧移动的距离范围。
  • list-data:传入弹幕数据列表。

源码:

github: vue3-dan-mu-view 伸手党同学 如果好用 还请点赞收藏哈 感谢

实现方案

这里可以分为两部分:
单条弹幕如何实现,需要注意哪些知识点。
多条弹幕实现 如何设计数据的缓存队列 dom池的复用逻辑。

单条弹幕实现

初始态:

截屏2024-11-30 16.36.05.png

结束态:

截屏2024-11-30 16.36.09.png

整体在X轴方向移动了 容器的宽度 + 自身宽度 的距离。
在vue中想要获取dom的width 可以通过ref实现。
位移动画 可以使用transform实现。

<div ref="container" :style="[props.innerStyle, _style]" class="scroll-row">
        <div ref="content" class="scroll-content" :style="{ transform: `translateX(${translateX}px)` }">
            <slot :data="_item">
                {{ _item }}
            </slot>
        </div>
    </div>
containerWidth = container.value?.offsetWidth || 0;
contentWidth = content.value?.offsetWidth || 0;
translateX.value = containerWidth;

内容自定义

每个item具体的样式 我们通过Vue 提供的插槽能力来实现

插槽 (slot) 特性的设计主要是为了实现组件内容的可重用和内容分发。这是一种让父组件可以向子组件动态插入内容的特性。

  1. 内容分发 (Content Projection) :插槽可以让开发者在使用一个组件的时候,向组件内部动态传入任意的 DOM 结构。这在开发类似于 Dialog、Card 这样的容器类型组件的时候非常有用。
  2. 作用域插槽 (Scoped Slot) :通过作用域插槽,父组件可以获取到子组件内部的数据,达到更复杂的定制和控制

对于插槽的应用不熟悉的同学 可以参考这篇文章 面试官:vue插槽有什么用?插槽的本质是什么?

item长度不定 如何解决长度问题?

因为内容是通过插槽用户自定义的 高度一般是固定的 但是长度都是不定的

我们要如何计算出每一个弹幕的长度 然后实现动画 移动到终态的效果呢?

很简单 设置translateX=-contentWidth 即可。

过渡效果

从初始态 移动到 结束态的中间过渡的过程 我们可以通过不断的修改translateX的值来实现。

这个过程会频繁的调用 这里我们使用 raf 来实现。 以保证动画和渲染频率一致。

window.requestAnimationFrame()  告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。rAF是浏览器提供的和渲染帧率保持同步的一个api.

具体参考 浏览器: 深入理解requestAnimationFrame优化js运行时

 const step = () => {
            translateX.value -= _frame.value;
            if (translateX.value < -contentWidth) {
                nextTick(() => {
                    emit('onEnd');
                });
            } else {
                animationFrameId = requestAnimationFrame(step);
            }
        };
        step();

当运行结束 我们向外抛出一个onEnd事件 让调用方可以感知到 以便实现组件数据清空并重复使用。

多条弹幕实现

截屏2024-11-30 16.13.39.png

数据缓存队列

因为数据一般是源源不断从服务器拉取的 新数据到达后 因为Vue的响应性 数据发生改变后 视图会自动更新 这可能会导致视图闪烁 带来较差的视觉体验 所以需要在服务端数据和实际应用的数据中间加一个缓存队列 服务端数据不断向这个队列累加 当视图中某个view动画运行结束 在依次从缓存队列中取新的数据。

dom缓存池

在初始化组件时传入的count属性指定了 这个dom缓存池的数量 组件最多同时出现的DOM数量即为count值。

当一个dom动画运行结束 且没有更多数据可以运行时 这个dom会被加入到空闲队列中 等下次有新的数据入队时 在继续运行动画。 并不会重复的创建销毁DOM 组件通过这个缓存池实现DOM复用 以达到性能优化的目的。

const handleScrollEnd = (instance: ItemIns) => {
  const item = list.value.shift();
  if (item) {
    instance.update(item);
  } else {
    idleList.push(instance);
  }
};

注意:如果初始化指定的count=10 实际传入的数据长度是5 那么组件依然会实例化10个item并且这10个item会正常的执行动画 只是会将多余的5个设置为重复的数据 并且设置一个较大的位移 在视图上不可见

建议根据视图的高度和弹幕的轨道高度 保证每个轨道有1~2个弹幕即可。