如何丝滑的实现一定数量,不固定高度的无限滚动

|前言

最近接到一个需求,要实现精选的用户评论在大屏电视上竖向无限滚动播放,当时第一个想法就是通过swiper实现,只需要简单的将几个参数拼接在一起就能实现。实际上拼接完各个需要的属性之后,发现并不能实现我所期待的效果,无奈之下只能自己手写一个实现该功能。(该功能其实也不难,只是一开始就想着借助第三方组件库来实现,没有去思考实现思路)

|简介

本次功能的实现使用了Window.requestAnimationFrame()方法实现,用到的框架是react。整体的实现思路是:拷贝两份数据,判断容器的 滚动高度 + 可视高度 = 整体高度,可以得出容器已经触底,这个时候再拷贝一份数据放进容器中继续滚动,同时删除第一份数据(这里删除第一份数据的原因是防止页面DOM节点过多)

|API

这里简单介绍一下 Window.requestAnimationFrame(),Window.requestAnimationFrame()告诉浏览器器------你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

|敲重点

调用该方法只能帮我们执行一次我们传入的回调函数,所以在我们传入的回调函数中需要再次主动去调用该方法。

语法:

scss 复制代码
requestAnimationFrame(callback)

参数:

arduino 复制代码
callback  // 需要执行的动画函数

返回值:

arduino 复制代码
一个整数ID  // 可以用来取消动画的执行

取消动画

javascript 复制代码
window.cancelAnimationFrame(ID)

|为什么不使用定时器而使用requestAnimationFrame

这里我们需要知道这两者的区别:

  1. 定时器可以帮助我们隔一段时间执行一段逻辑代码,理论上也是可以实现定时滚动一定距离的需求的,但是问题就在于每次执行动画函数时不能保证动画执行在浏览器进行重绘之前,无法卡在浏览器的更新动画帧上。简单来说就是:定时执行的滚动,可能在浏览器重新渲染之前,也可能在浏览器重新渲染之后,导致的结果就是页面滚动不流畅,有时候看起来没滚动,有时候看起来滚动的距离又比较大,会有抖动的问题。
  2. requestAnimationFrame方法可以保证每次动画的执行都完美卡在浏览器重绘之前,使得同样是滚动却可以非常流畅。

|代码实现

js 复制代码
import React, { useRef, useEffect } from 'react'
// 样式
import styles from './index.scss'
// 列表数据
import { userList } from './utils'

// 滚动容器
const wrapperRef = useRef()
// 是否已经开启动画
const flagRef = useRef(false)
// 所有滚动item容器
const allItemRef = useRef()
// 保存动画ID
const animationRef = useRef()

// 右边部分元素无限滚动
  function nodeMove() {
    const container = wrapperRef.current // 容器元素
    const allNode = allItemRef.current // 全部元素
    container.appendChild(allNode.cloneNode(true)) // 复制元素到后方,实现两份数据
    const allNodeHeight = container.scrollHeight // 获取全部元素高度
    const loop = () => {
      if(container.scrollTop + container.offsetHeight >= wrapperRef.current?.scrollHeight) {
        // 触底则拷贝一份元素加进去,并删除已经隐藏的第一份数据
        console.log('到底了');
        container.appendChild(allNode.cloneNode(true))
        container.removeChild(container.firstChild)
      } else {
        // 没有触底则正常滚动
        container.scrollTop++
      }
      animationRef.current = window.requestAnimationFrame(() => loop()) // 动画递归调用
    }
    animationRef.current = window.requestAnimationFrame(() => loop()) // 启动动画
    flagRef.current = true
  }

  useEffect(() => {
    if(!flagRef.current && allItemRef.current) {
      nodeMove()
    }
    window.addEventListener('fullscreenchange', watchFullState)
    return () => {
      window.removeEventListener('fullscreenchange', watchFullState)
    }
  }, [])

// DOM结构

// 最外层容器,需要设置超出部分隐藏
<div className={styles['swiper-container']}>
  // 滚动容器
  <div className={styles['swiper-wrapper-container']} ref={wrapperRef}>
    // 所有item的容器,用来获取所有item的总高度,判断是否触底
    <div ref={allItemRef}>
      {
        // 以下是所有item,大家可根据需要编写结构和样式
        userList.map((item, index) => (
          <div key={index} className={styles['swiper-item']}>
            <div className={styles['item-header']}>
              <div className={styles['header-left']}>
                <div className={styles['header-left-img']}>
                  <img src={item.avatarUrl}></img>
                </div>
                <p className={styles['header-left-name']}>{item.username}</p>
              </div>
              <div className={styles['header-right']}>
                <p className={styles['header-right-title']}>{item.title}</p>
                <p className={styles['header-right-time']}>{item.time}</p>
              </div>
            </div>
            <div className={styles['item-bottom']}>
              {
                item.detail.map((value, idx) => (
                  <p key={idx}>{value}</p>
                ))
              }
            </div>
          </div>
        ))
      }
    </div>
  </div>
</div>
scss 复制代码
  .swiper-container {
    width: 100%;
    height: calc(100% - 81px);
    overflow: hidden;
  }
  .swiper-wrapper-container {
    width: 100%;
    height: 100%;
    overflow-y: auto;
    &::-webkit-scrollbar {
      width: 0 !important;
      display: none !important;
    }
    &::-webkit-scrollbar-track {
      display: none !important;
    }
    &::-webkit-scrollbar-button {
      display: none !important;
    }
  }
  .swiper-item {
    background: rgba(0,172,231,0.08);
    border-radius: 23px;
    padding-top: 12px;
    padding-bottom: 18px;
    margin-bottom: 15px;
    .item-header {
      border-bottom: 1px solid rgba(0, 172, 231, .2);
      padding-left: 18px;
      padding-bottom: 11px;
      display: flex;
      .header-left {
        width: 105px;
        .header-left-img {
          width: 53px;
          height: 53px;
          margin-left: 24px;
          border-radius: 50%;
          overflow: hidden;
          img {
            display: block;
            width: 100%;
            height: 100%;
          }
        }
        .header-left-name {
          margin-top: 5px;
          text-align: center;
          font-size: 14px;
          font-weight: 400;
          color: #000000;
          opacity: .8;
          line-height: 19px;
        }
      }
      .header-right {
        margin-left: 18px;
        padding-top: 12px;
        .header-right-title {
          font-size: 20px;
          font-weight: 600;
          color: #000000;
          opacity: .8;
          line-height: 28px;
          margin-bottom: 17px;
        }
        .header-right-time {
          font-size: 14px;
          opacity: .3;
          font-weight: 400;
          color: #000000;
          line-height: 19px;
        }
      }
    }
    .item-bottom {
      padding: 11px 18px 0 18px;
      p {
        font-size: 17px;
        font-weight: 400;
        opacity: .6;
        color: #000000;
        line-height: 23px;
      }
    }
  }

|总结

除了通过控制滚动高度,也可以通过控制容器的margin-top为负值来实现向上滚动,当然触底的判断和细节处理也有所不同,本次就只讲解通过控制滚动高度来实现无限轮播,由于页面涉及公司内部业务就不在这里展示了,大家可以复制代码在本地修改下数据跑一下看看实现效果。

如果大家有更好的实现方案也可以在评论区讨论!!欢迎大家提出更多的方案一起学习。

相关推荐
zqx_715 分钟前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己32 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称1 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色1 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2342 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河2 小时前
CSS总结
前端·css
NiNg_1_2342 小时前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普2 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠2 小时前
如何通过js加载css和html
javascript·css·html