自己对列表的自动滚动理解,在项目的需求上用到了就记录一下
<template>
<!-- 基本用法 -->
<div v-auto-scroll class="scroll-box"></div>
<!-- 自定义配置 -->
<div v-auto-scroll="{
speed: 300,
smoothReset: false
}" class="scroll-box"></div>
</template>
<style>
.scroll-box {
height: 400px;
overflow-y: auto;
/* 必须指定高度 */
}
</style>
Vue.directive('autoscroll', {
inserted(el, binding) {
console.log('autoscroll',el, binding)
const defaultOptions = {
speed: 200, // 滚动速度(px/s)
smoothReset: true, // 是否平滑重置
pauseOnHover: true, // 悬停暂停
pauseOnClick: true, // 点击暂停
pauseOnScroll: true // 滚动暂停
}
// 合并配置参数
const options = {
...defaultOptions,
...(binding.value || {})
}
// 计算帧间隔时间
const gap = 1000 / (options.speed / 2)
let animationFrameId = null
let isUsing = false
const listeners = []
// 事件处理器工厂
const createHandler = (type, handler) => {
const wrapper = (e) => {
if (typeof handler === 'function') handler(e)
}
listeners.push({ type, handler: wrapper })
return wrapper
}
// 平滑滚动到底部
const smoothScrollToBottom = () => {
const start = el.scrollTop
const end = el.scrollHeight - el.clientHeight
const duration = 500 // 毫秒
const animate = (timestamp) => {
const runtime = timestamp - startTime
const progress = Math.min(runtime / duration, 1)
el.scrollTop = start + (end - start) * progress
if (runtime < duration) {
animationFrameId = requestAnimationFrame(animate)
}
}
const startTime = performance.now()
animationFrameId = requestAnimationFrame(animate)
}
// 主滚动动画
const scrollAnimation = () => {
let lastTime = 0
let accumulatedDiff = 0
const animate = (timestamp) => {
if (!lastTime) lastTime = timestamp
const delta = timestamp - lastTime
accumulatedDiff += delta
lastTime = timestamp
if (isUsing) {
cancelAnimationFrame(animationFrameId)
return
}
if (accumulatedDiff >= gap) {
const remaining = el.scrollHeight - (el.scrollTop + el.clientHeight)
if (remaining > 1) {
el.scrollTop += 1
} else if (options.smoothReset) {
smoothScrollToBottom()
return
} else {
el.scrollTop = 0
}
accumulatedDiff = 0
}
animationFrameId = requestAnimationFrame(animate)
}
animationFrameId = requestAnimationFrame(animate)
}
// 事件监听
if (options.pauseOnHover) {
el.addEventListener('mouseenter', createHandler('mouseenter', () => {
isUsing = true
}), { passive: true })
el.addEventListener('mouseleave', createHandler('mouseleave', () => {
isUsing = false
scrollAnimation()
}), { passive: true })
}
if (options.pauseOnScroll) {
el.addEventListener('wheel', createHandler('wheel', () => {
isUsing = true
}), { passive: true })
}
if (options.pauseOnClick) {
el.addEventListener('click', createHandler('click', () => {
isUsing = true
}), { passive: true })
}
// 初始化滚动
scrollAnimation()
// 响应式尺寸变化
const resizeObserver = new ResizeObserver(() => {
cancelAnimationFrame(animationFrameId)
scrollAnimation()
})
resizeObserver.observe(el)
// 保存引用用于卸载
el._autoScroll = {
resizeObserver,
options
}
},
unmounted(el) {
// 清理所有资源
cancelAnimationFrame(el._autoScroll?.animationFrameId)
el._autoScroll?.resizeObserver.disconnect()
// 移除事件监听
el._autoScroll?.listeners.forEach(({ type, handler }) => {
el.removeEventListener(type, handler)
})
}
})