封装一个自动循环滚动的列表

1,075 阅读3分钟

前言

在做数据大屏开发的过程中,经常出现需要对列表进行自动滚动的需求,为了个性化的实现各种需求,就封装了一个函数方法

屏幕录制2023-09-13 22.04.31.gif

解决方案

  1. 生成一个父元素,并给其固定的高度,通过$refs获取并赋值给container变量
  2. 获取一些关于container和其子元素的信息,如父容器高度(parentHeight)、滚动高度(scrollHeight)、第一个子元素的高度(childHeight)、子元素数量(childCount)、最大高度(maxHeight)、当前滚动位置(toHeight)和当前子元素索引(current),并初始化一个名为timer的计时器。
  3. 定义一个名为scrollCore的函数,该函数用于实现滚动效果。
  4. scrollCore函数中检查如果scrollHeight大于parentHeight,则创建一个定时器,以便每隔一段时间执行一次滚动操作。
  5. 在定时器中,scrollCore函数中逐渐增加容器的scrollTop属性,从而实现滚动效果。

一、匀速自动滚动,并实现方法可对多个列表复用

思路:通过对ref的命名,来实现在方法中使用for循环来对多个列表生效

<template>
<div>
    <ul class="shiJianGaoJingContent_right" ref="scroll0">
        <li class="gaojing_info_list" v-for="(item, index) in scrollInfoList" :key="index">
        </li>
    </ul> 
</div>
</template>

<script>
export default {
  data() {
    return {
      scrollInfoList:[],
    };
  },
  methods: {
  // 列表自动滚动
    scrollFn() {
      let dataList = [];
      for (let i = 0; i < 1; i++) {
        let container = this.$refs["scroll" + i];
        let parentHeight = container.offsetHeight || 0;
        let scrollHeight = container.scrollHeight;
        let childHeight = container.firstChild.offsetHeight;
        let childCount = container.childNodes.length;
        let maxHeight = childHeight * childCount;
        let toHeight = container.scrollTop;
        let current = 0;

        dataList.push({
          parentHeight,
          scrollHeight,
          childHeight,
          childCount,
          maxHeight,
          toHeight,
          current,
          timer: null,
          timerMouseWheel: null,
        });
        function scrollCore() {
          {
            if (scrollHeight > parentHeight) {
              dataList[i].timer = setInterval(() => {
                let scrollStep = 1; // 每帧滚动的步长,可以适当调整

                if (container.scrollTop + parentHeight + 5 >= scrollHeight) {
                  // 如果滚动到底部,则将滚动位置重置为0
                  container.scrollTop = 0;
                  dataList[i].toHeight = 0;
                  dataList[i].current = 0;
                } else {
                  if (container.scrollTop < dataList[i].toHeight + dataList[i].childHeight) {
                    // 如果尚未滚动到目标位置,则逐帧更新滚动位置
                    container.scrollTop += scrollStep;
                  } else {
                    // 已滚动到目标位置,更新当前子项索引和子项高度
                    dataList[i].current = (dataList[i].current + 1) % dataList[i].childCount;
                    dataList[i].childHeight = container.childNodes[dataList[i].current].offsetHeight;
                    dataList[i].toHeight = container.scrollTop;
                  }
                }
              }, 2000 / 60); // 设置每秒更新60次,可根据需求调整
            }
          }
        }
        scrollCore()
      }
    },
    
  },
  mounted() {},
};
</script>

注:该方法在自动滚动到底部后会返回到顶部,循环滚动需要更改逻辑,在下方有循环的方法。

二、如何暂时关闭自动滚动

当我们想自己通过鼠标滚轮滚动该列表时,使该列表的自动滚动暂停,我们可以添加了一个名为mousewheel的事件监听器,以便在鼠标滚轮滚动时执行以下操作:

  1. 清除之前的定时器dataList[i].timerdataList[i].timerMouseWheel
  2. 更新dataList[i].toHeight为当前滚动位置,并将滚动位置设置为相同的值。
  3. 设置一个新的定时器dataList[i].timerMouseWheel,在一段时间后重新调用scrollCore函数,以恢复自动滚动效果。
 container.addEventListener("mousewheel", () => {
          this.$nextTick(() => {
            clearInterval(dataList[i].timer);
            clearInterval(dataList[i].timerMouseWheel);
            dataList[i].toHeight = container.scrollTop;
            container.scrollTop = container.scrollTop;
            dataList[i].timerMouseWheel = setTimeout(() => {
              scrollCore()
            }, 3000)

          });
        });

二、如何彻底关闭自动滚动

在上面的方法中会一直无限的滚动下去,那么我们如果想停下具体看某一项要把自动滚动关闭掉呢,我们可以通过将关闭方法写在container的某个事件中,并将该事件派发给container来实现

// 监听关闭自动滚动的事件
container.addEventListener("closeScroll", () => {
          this.$nextTick(() => {
            clearInterval(dataList[i].timer);
            clearInterval(dataList[i].timerMouseWheel);
            toHeight = container.scrollTop;
            container.scrollTop = container.scrollTop;
          });
        });
// 完整代码
// 关闭列表自动滚动
    closeListScroll(container) {
      // 创建一个新的 自定义 事件
      const closeScroll = new Event("closeScroll");
      container.dispatchEvent(closeScroll)
    },
// 列表自动滚动
    scrollFn() {
      let dataList = [];
      for (let i = 0; i < 1; i++) {
        let container = this.$refs["scroll" + i];
        let parentHeight = container.offsetHeight || 0;
        let scrollHeight = container.scrollHeight;
        let childHeight = container.firstChild.offsetHeight;
        let childCount = container.childNodes.length;
        let maxHeight = childHeight * childCount;
        let toHeight = container.scrollTop;
        let current = 0;

        dataList.push({
          parentHeight,
          scrollHeight,
          childHeight,
          childCount,
          maxHeight,
          toHeight,
          current,
          timer: null,
          timerMouseWheel: null,
        });
        function scrollCore() {
          {
            if (scrollHeight > parentHeight) {
              dataList[i].timer = setInterval(() => {
                let scrollStep = 1; // 每帧滚动的步长,可以适当调整

                if (container.scrollTop + parentHeight + 5 >= scrollHeight) {
                  // 如果滚动到底部,则将滚动位置重置为0
                  container.scrollTop = 0;
                  dataList[i].toHeight = 0;
                  dataList[i].current = 0;
                } else {
                  if (container.scrollTop < dataList[i].toHeight + dataList[i].childHeight) {
                    // 如果尚未滚动到目标位置,则逐帧更新滚动位置
                    container.scrollTop += scrollStep;
                  } else {
                    // 已滚动到目标位置,更新当前子项索引和子项高度
                    dataList[i].current = (dataList[i].current + 1) % dataList[i].childCount;
                    dataList[i].childHeight = container.childNodes[dataList[i].current].offsetHeight;
                    dataList[i].toHeight = container.scrollTop;
                  }
                }
              }, 2000 / 60); // 设置每秒更新60次,可根据需求调整
            }
          }
        }
        scrollCore()

        container.addEventListener("mousewheel", () => {
          this.$nextTick(() => {
            clearInterval(dataList[i].timer);
            clearInterval(dataList[i].timerMouseWheel);
            dataList[i].toHeight = container.scrollTop;
            container.scrollTop = container.scrollTop;
            dataList[i].timerMouseWheel = setTimeout(() => {
              scrollCore()
            }, 3000)

          });
        });
        container.addEventListener("closeScroll", () => {
          this.$nextTick(() => {
            clearInterval(dataList[i].timer);
            clearInterval(dataList[i].timerMouseWheel);
            toHeight = container.scrollTop;
            container.scrollTop = container.scrollTop;
          });
        });
      }
    },

通过如上代码 我们就可以通过调用closeListScroll()方法来关闭列表自动滚动,如我们想要关闭ref=scroll0列表的自动滚动

// 示例 关闭ref=scroll0列表的自动滚动
// 某个方法中
clickSomeBtn(){
    ... //其他逻辑
    this.closeListScroll(this.$refs["scroll0"])
}

三、如何使自动滚动无限循环,使其头尾相连

思路:将一份数据复制为两份,在滚动到第二份与第一份重合的时候 立刻将滚动高度归位,这样从视觉效果上来看,就是无限滚动的效果 要实现这个效果,首先是我们需要将一份数据变为两份,最为简单的实现思路为直接将数据变为两份

<li class="gaojing_info_list" v-for="(item, index) in [...scrollInfoList,...scrollInfoList]" :key="index">

但是这样的话,我们需要对所有的列表都进行更改,容易遗漏,不符合封装思路
于是我就想着通过DOM方法直接在封装函数中进行操作,实现思路为

  1. 使用DOM方法获取container的所有子元素,并将它们存储在名为children的变量中。
  2. 创建一个文档片段(Document Fragment)并将子元素逐个克隆并添加到文档片段中。
  3. 一次性将文档片段添加回container中,以提高性能。 当我们实现了克隆两份数据后,通过对container.scrollTop >= scrollHeight / 2的判断,来得到我们已经来到了第二页与初始位置重复的位置,在这个时候将滚动位置重置,在视觉上就会实现首尾相连无限滚动的效果

优化

  1. 通过requestAnimationFrame对滚动性能进行优化
  2. 有时候并不清楚后端返回的数据是否能达到容器的高度,所以需要通过对初始的scrollHeightparentHeight进行判断来处理未填满容器的逻辑
  3. 当每帧滚动的步长已无法下调但依然过快时,设置requestAnimationFrame的帧率来处理速率过快的问题
scrollFn() {
  let dataList = []; // 用于存储滚动相关的数据

  // 循环处理5个滚动容器
  for (let i = 0; i < 5; i++) {
    let container = this.$refs["scroll" + i]; // 获取当前滚动容器的引用

    let oldParentHeight = container.offsetHeight; // 获取滚动容器的初始高度
    let oldScrollHeight = container.scrollHeight; // 获取滚动容器的初始滚动高度

    // 使用 DOM 方法获取所有子元素
    let children = container.children;

    // 创建一个文档片段,用于重新插入子元素
    let fragment = document.createDocumentFragment();

    // 将子元素逐个克隆并插入文档片段
    for (let ind = 0; ind < children.length; ind++) {
      const child = children[ind].cloneNode(true);
      fragment.appendChild(child);
    }

    // 如果滚动高度超过容器高度,将文档片段一次性添加回容器中
    if (oldScrollHeight > oldParentHeight) {
      container.appendChild(fragment);
    }

    // 获取滚动容器的各种属性
    let parentHeight = container.offsetHeight;
    let scrollHeight = container.scrollHeight;
    let childHeight = container.firstChild.offsetHeight;
    let childCount = container.childNodes.length;
    let toHeight = container.scrollTop;
    let current = 0;

    // 将滚动容器的属性保存到 dataList 数组中
    dataList.push({
      parentHeight,
      scrollHeight,
      childHeight,
      childCount,
      toHeight,
      current,
    });

    // 定义 scrollCore 函数,处理滚动逻辑
    function scrollCore() {
      let animationFrameInterval = 1000 / 60; // 设置每秒30帧的动画间隔

      // 只有当滚动高度超过容器高度时才进行滚动
      if (oldScrollHeight >= oldParentHeight) {
        function animateScroll(timestamp) {
          let scrollStep = 1; // 每帧滚动的步长,可以适当调整

          if (!dataList[i].lastTimestamp) {
            dataList[i].lastTimestamp = timestamp;
          }

          // 根据时间间隔计算是否需要进行滚动
          if (timestamp - dataList[i].lastTimestamp >= animationFrameInterval) {
            dataList[i].lastTimestamp = timestamp;

            // 根据滚动位置和目标位置进行滚动
            if (container.scrollTop >= scrollHeight / 2) {
              container.scrollTop = 0;
              dataList[i].toHeight = 0;
              dataList[i].current = 0;
            } else {
              if (
                container.scrollTop <
                dataList[i].toHeight + dataList[i].childHeight
              ) {
                // 如果剩余的滚动距离小于 scrollStep,则直接滚动到目标位置
                if (
                  container.scrollTop + scrollStep >=
                  dataList[i].toHeight + dataList[i].childHeight
                ) {
                  container.scrollTop =
                    dataList[i].toHeight + dataList[i].childHeight;
                } else {
                  container.scrollTop += scrollStep;
                }
              } else {
                dataList[i].current =
                  (dataList[i].current + 1) % dataList[i].childCount;
                dataList[i].childHeight =
                  container.childNodes[dataList[i].current].offsetHeight;
                dataList[i].toHeight = container.scrollTop;
              }
            }
          }

          // 继续滚动
          dataList[i].animationFrameId = requestAnimationFrame(animateScroll);
        }

        // 开始滚动
        animateScroll();
      }
    }

    // 调用 scrollCore 函数开始滚动
    scrollCore();

    // 添加鼠标滚轮事件监听器,用于停止滚动和计时器
    container.addEventListener("mousewheel", () => {
      this.$nextTick(() => {
        cancelAnimationFrame(dataList[i].animationFrameId);
        clearInterval(dataList[i].timerMouseWheel);
        dataList[i].toHeight = container.scrollTop;
        container.scrollTop = container.scrollTop;
        dataList[i].timerMouseWheel = setTimeout(() => {
          scrollCore();
        }, 3000);
      });
    });
    // 监听彻底关闭滚动的事件
    container.addEventListener("closeScroll", () => {
      this.$nextTick(() => {
        cancelAnimationFrame(dataList[i].animationFrameId);
        clearInterval(dataList[i].timerMouseWheel);
        toHeight = container.scrollTop;
        container.scrollTop = container.scrollTop;
      });
    });
  }
}