😎 小程序手搓轮播图,几千个元素滑动照样丝滑~

前言

在最近做小程序的时候,遇到了轮播图卡顿的情况。有些情况下,轮播图需要展示上千个元素,如果使用原生的 Swiper 组件,渲染这么多元素会导致严重的卡顿问题。

原生的 Swiper 组件无法做到仅展示当前元素和前后两个元素。虽然你可以控制 swiper-item 的内容不展示,但 swiper-item 这个 DOM 节点仍然会被渲染。因此,在几千个元素的情况下,轮播图组件依然会非常卡顿。

开始介绍解决方法前先给大家看看我实现的效果如何:

优化原理

优化的核心思想是按需渲染,即只渲染当前可见的区域以及前后两张卡片,确保滚动时不会出现空白或卡顿。这种方式类似于懒加载,动态加载和卸载内容,从而减少 DOM 节点的数量,提升性能。

我们需要在一个有限的屏幕上展示大量图片,但无法一次性加载所有图片。于是,我们只加载当前屏幕可见的图片以及前后几张图片,当用户滑动时,动态加载新的图片并卸载不再可见的图片。这样既能保证流畅的滑动体验,又能避免一次性加载过多内容导致的性能问题。

当用户滑动到第一百张卡片时,我们依然只渲染三张卡片:当前卡片、前一张卡片和后一张卡片。屏幕外的卡片会在滑动时动态加载和卸载:

实现

接下来,我将介绍如何实现一个支持按需渲染的轮播图组件。下面的代码已经将业务逻辑抽离,只保留了核心代码来展示整体实现思路。

虚拟列表渲染

ts 复制代码
// 只渲染当前页及其相邻页面
const getRenderItems = (current: number, list: any[]) => {
  const minIndex = Math.max(0, current - 1)
  const maxIndex = Math.min(list.length, current + 1)
  
  return list.slice(minIndex, maxIndex + 1).map(item => ({
    ...item,
    index: list.findIndex(i => i.id === item.id)
  }))
}
  • 只渲染当前页及其相邻页面:通过计算最小和最大索引,确保只渲染当前页及其前后相邻的页面,减少不必要的渲染开销。

布局结构

ts 复制代码
<template>
  <view class="container" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
    <!-- 滑动容器 -->
    <view 
      class="slider"
      :class="[!initializing && 'transition', isDragging && 'dragging']"
      :style="{ transform: `translateX(${translateX}px)` }"
    >
      <!-- 占位容器 -->
      <view 
        class="placeholder"
        :style="{ width: `${(currentIndex - 1) * width}px` }"
      />
      
      <!-- 渲染项 -->
      <view 
        v-for="item in renderItems"
        :key="item.id"
        :style="{ width: `${width}px` }"
      >
        <slot :item="item" />
      </view>
    </view>
  </view>
</template>
  • 滑动容器 :通过 transform 实现水平滑动,使用 translateX 控制位移。
  • 占位容器:用于保持滑动时的正确位置偏移,避免页面跳动。
  • 渲染项 :根据 renderItems 动态渲染当前页及其相邻页面的内容。

关于占位容器,这里我解释一下它的作用方便大家理解,随着页面不断往左边滚动,轮播图的整体容器会向左偏移越来越多,为了让当前轮播元素展示在正中间,我们需要在左侧增加一个动态增大的占位容器,下面我画了一个图:

这个状态是滚动到卡片2时,实际的元素渲染状态,如果我们要让容器平滑的往右滚动查看卡片3,当卡片3出现的时候,卡片1就消失了,并且需要提前渲染卡片4,此时就需要占位容器来把卡片1的位置占住,这样我们的 translateX 偏移效果才能刚好让卡片 3 展示出来

同样的,继续往后滚动到卡片4的时候,继续加大占位容器的宽度:

除了占位容器的方法,你也可以使用 absolute 布局,动态设置卡片的 left 偏移,这样就不需要增加占位容器,也能实现平滑的滚动效果。不过,我当时觉得占位容器的方式更简单,且最多只渲染四个元素,因此选择了这种方式。这里提供的只是一个思路。

触摸事件处理

ts 复制代码
// 状态管理
const state = reactive({
  startX: 0,
  translateX: 0,
  isDragging: false,
  currentIndex: 0
})

// 触摸开始
const onTouchStart = (e: TouchEvent) => {
  state.startX = e.touches[0].clientX
  state.isDragging = true
}

// 触摸移动
const onTouchMove = (e: TouchEvent) => {
  if (!state.isDragging) return
  
  const deltaX = e.touches[0].clientX - state.startX
  const moveRatio = 0.5 // 移动阻尼系数
  const edgeRatio = 0.12 // 边缘阻尼系数
  
  // 边缘阻尼处理
  if ((state.currentIndex === 0 && deltaX > 0) || 
      (state.currentIndex === list.length - 1 && deltaX < 0)) {
    state.translateX = -state.currentIndex * width + deltaX * edgeRatio
  } else {
    state.translateX = -state.currentIndex * width + deltaX * moveRatio
  }
}

// 触摸结束
const onTouchEnd = (e: TouchEvent) => {
  if (!state.isDragging) return
  
  const deltaX = e.changedTouches[0].clientX - state.startX
  const threshold = width * 0.1 // 滑动阈值
  
  let newIndex = state.currentIndex
  
  // 判断是否切换页面
  if (Math.abs(deltaX) > threshold) {
    if (deltaX > 0 && state.currentIndex > 0) {
      newIndex--
    } else if (deltaX < 0 && state.currentIndex < list.length - 1) {
      newIndex++
    }
  }
  
  state.isDragging = false
  state.translateX = -newIndex * width
  
  if (newIndex !== state.currentIndex) {
    state.currentIndex = newIndex
    emit('change', newIndex)
  }
}
  • 添加移动阻尼,提升滑动体验:通过阻尼系数控制滑动速度,使滑动更加平滑。
  • 边缘回弹效果:当滑动到列表边缘时,添加回弹效果。
  • 基于阈值的页面切换判断:根据滑动距离是否超过阈值来决定是否切换页面。
  • 使用 transform 代替 left/top 实现位移 :通过 transform 实现位移,性能更好。
  • 拖动时禁用过渡动画:在拖动过程中禁用过渡动画,避免动画干扰拖动效果。

过渡动画处理

css 复制代码
.slider {
  display: flex;
  height: 100%;
}

.slider.transition {
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.slider.dragging {
  transition: none;
}

初次进入页面

要维护一个状态,初次进入页面时不执行动画,否则如果初次进入页面查看的是很靠后的卡片,会有一个快速滑动到当前卡片的效果,卡片太多时会闪烁一下,体验不太好。

ts 复制代码
// 添加一个控制初始化动画的变量
const initializing = ref(true)

onLoad((query: { index: string; }) => {
  // 设置初始位置(无动画)
  translateX.value = -(parseInt(query.index) || 0) * windowWidth

  // 等待下一帧后启用动画
  nextTick(() => {
    initializing.value = false
  })

总结

通过按需渲染的方式,我们成功极大轮播图的性能,避免了在渲染大量元素时的卡顿问题。这种方式不仅适用于小程序,也可以应用于其他前端框架。

如果你想实际体验一下实现的效果,可以在学习卡盒小程序,在任意卡盒中的卡片点击后进入大卡片页面,左右滑动一下看看是不是你想要的效果。希望这篇文章能为你提供一些有用的思路和实现方法!

相关推荐
癞皮狗不赖皮11 分钟前
WEB 攻防-通用漏-XSS 跨站脚本攻击-反射型/存储型/DOM&BEEF-XSS
前端·网络·网络安全·xss
明月看潮生16 分钟前
青少年编程与数学 02-006 前端开发框架VUE 24课题、UI表单
javascript·vue.js·ui·青少年编程·编程与数学
Stanford_110620 分钟前
AI大模型如何赋能电商行业并引领变革?
大数据·人工智能·微信小程序·微信公众平台·twitter·微信开放平台
一棵开花的树,枝芽无限靠近你23 分钟前
【PPTist】幻灯片放映
前端·笔记·学习·编辑器·pptist
前端熊猫27 分钟前
CSS3的aria-hidden学习
前端·学习·css3
众多菜狗中的一只菜狗一只28 分钟前
threejs中的相机与物体
前端·javascript·空间计算
癞皮狗不赖皮41 分钟前
WEB攻防-通用漏洞_XSS跨站_权限维持_捆绑钓鱼_浏览器漏洞
前端·web安全·网络安全·xss
周尛先森42 分钟前
专属于你的 Vue3 小指南
前端
ss2731 小时前
基于Springboot + vue实现的文档管理系统
vue.js·spring boot·后端
_未知_开摆1 小时前
CSS | 实现三列布局(两边边定宽 中间自适应,自适应成比)
前端·css·vue.js·vue·html·css3·html5