IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 适配vue3的滚动列表插件 -> 正文阅读

[JavaScript知识库]适配vue3的滚动列表插件

在vue3中想使用滚动列表插件的时候发现,vue-seamless-scroll并不支持vue3版本。

所以这里介绍一下适配vue3版本的滚动列表插件,采用的这位前辈的源代码,技术使用的是tsx。

完整代码

import { computed, CSSProperties, defineComponent, onBeforeMount, onMounted, ref, watch } from 'vue'

const props = {
  // 是否开启自动滚动
  modelValue: {
    type: Boolean,
    default: true,
  },
  // 原始数据列表
  list: {
    type: Array,
    required: true,
  },
  // 步进速度,step 需是单步大小的约数
  step: {
    type: Number,
    default: 1,
  },
  // 开启滚动的数据量
  limitScrollNum: {
    type: Number,
    default: 1,
  },
  // 是否开启鼠标悬停
  hover: {
    type: Boolean,
    default: false,
  },
  // 控制滚动方向
  direction: {
    type: String,
    default: 'up',
  },
  // 单步运动停止的高度
  singleHeight: {
    type: Number,
    default: 0,
  },
  // 单步运动停止的宽度
  singleWidth: {
    type: Number,
    default: 0,
  },
  // 单步停止等待时间(默认值 1000ms)
  singleWaitTime: {
    type: Number,
    default: 1000,
  },
  // 是否开启 rem 度量
  isRemUnit: {
    type: Boolean,
    default: false,
  },
  // 开启数据更新监听
  isWatch: {
    type: Boolean,
    default: true,
  },
  // 动画时间
  delay: {
    type: Number,
    default: 0,
  },
  // 动画方式
  ease: {
    type: [String, Object],
    default: 'ease-in',
  },
  // 动画循环次数,-1表示一直动画
  count: {
    type: Number,
    default: -1,
  },
  // 拷贝几份滚动列表
  copyNum: {
    type: Number,
    default: 1
  }
}

const Vue3SeamlessScroll = defineComponent({
  name: 'vue3SeamlessScroll',
  inheritAttrs: false,
  props,
  emits: ['stop', 'count'],
  setup(props, { slots, emit, attrs }) {
    const scrollRef = ref(null)
    const slotListRef = ref<HTMLBodyElement>()
    const realBoxRef = ref<HTMLBodyElement>()
    const reqFrame = ref<number>(0)
    const singleWaitTimeout = ref(0)
    const realBoxWidth = ref(0)
    const realBoxHeight = ref(0)
    const xPos = ref(0)
    const yPos = ref(0)
    const isHover = ref(false)
    const _count = ref(0);

    // 如果列表长度大于最小滚动长度,就可以发生滚动
    const isScroll = computed(() => props.list!.length >= props.limitScrollNum)

    const realBoxStyle = computed<CSSProperties>(() => {
      return {
        width: realBoxWidth.value ? `${realBoxWidth.value}px` : 'auto',
        transform: `translate(${xPos.value}px,${yPos.value}px)`,
        // @ts-ignore
        transition: `all ${typeof props.ease === 'string' ? props.ease : 'cubic-bezier(' + props.ease.x1 + ',' + props.ease.y1 + ',' + props.ease.x2 + ',' + props.ease.y2 + ')'} ${props.delay}ms`,
        overflow: 'hidden'
      }
    })

    const isHorizontal = computed(
      () => props.direction == 'left' || props.direction == 'right'
    )

    const floatStyle = computed<CSSProperties>(() => {
      return isHorizontal.value
        ? { float: 'left', overflow: 'hidden' }
        : { overflow: 'hidden' }
    })

    const baseFontSize = computed(() => {
      return props.isRemUnit
        ? parseInt(
          globalThis.window.getComputedStyle(globalThis.document.documentElement, null).fontSize
        )
        : 1
    })

    const realSingleStopWidth = computed(
      () => props.singleWidth * baseFontSize.value
    )

    const realSingleStopHeight = computed(
      () => props.singleHeight * baseFontSize.value
    )

    const step = computed(() => {
      let singleStep: number
      let _step = props.step
      if (isHorizontal.value) {
        singleStep = realSingleStopWidth.value
      } else {
        singleStep = realSingleStopHeight.value
      }
      if (singleStep > 0 && singleStep % _step > 0) {
        console.error(
          "如果设置了单步滚动,step需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~"
        )
      }
      return _step
    })

    function cancle() {
      cancelAnimationFrame(reqFrame.value);
      reqFrame.value = 0;
    }

    function move() {
      cancle();
      if (isHover.value || !isScroll.value || _count.value === props.count) {
        emit('stop', _count.value);
        _count.value = 0;
        return;
      }
      reqFrame.value = requestAnimationFrame(function () {
        const h = realBoxHeight.value / 2
        const w = realBoxWidth.value / 2
        let { direction, singleWaitTime } = props
        if (direction === 'up') {
          if (Math.abs(yPos.value) >= h) {
            yPos.value = 0
            _count.value += 1
            emit('count', _count.value)
          }
          yPos.value -= step.value
        } else if (direction === 'down') {
          if (yPos.value >= 0) {
            yPos.value = h * -1
            _count.value += 1
            emit('count', _count.value)
          }
          yPos.value += step.value
        } else if (direction === 'left') {
          if (Math.abs(xPos.value) >= w) {
            xPos.value = 0
            _count.value += 1
            emit('count', _count.value)
          }
          xPos.value -= step.value
        } else if (direction === 'right') {
          if (xPos.value >= 0) {
            xPos.value = w * -1
            _count.value += 1
            emit('count', _count.value)
          }
          xPos.value += step.value
        }
        if (singleWaitTimeout.value) {
          clearTimeout(singleWaitTimeout.value)
        }
        if (!!realSingleStopHeight.value) {
          if (Math.abs(yPos.value) % realSingleStopHeight.value < step.value) {
            singleWaitTimeout.value = setTimeout(() => {
              move()
            }, singleWaitTime)
          } else {
            move()
          }
        } else if (!!realSingleStopWidth.value) {
          if (Math.abs(xPos.value) % realSingleStopWidth.value < step.value) {
            singleWaitTimeout.value = setTimeout(() => {
              move()
            }, singleWaitTime)
          } else {
            move()
          }
        } else {
          move()
        }
      });
    }

    function initMove() {
      if (isHorizontal.value) {
        let slotListWidth = slotListRef?.value?.offsetWidth;
        slotListWidth = slotListWidth! * 2 + 1;
        realBoxWidth.value = slotListWidth;
      }

      if (isScroll.value) {
        realBoxHeight.value = realBoxRef?.value?.offsetHeight!;
        if (props.modelValue) {
          move();
        }
      } else {
        cancle();
        yPos.value = xPos.value = 0;
      }
    }

    function startMove() {
      isHover.value = false;
      move();
    }

    function stopMove() {
      isHover.value = true;
      if (singleWaitTimeout.value) {
        clearTimeout(singleWaitTimeout.value);
      }
      cancle();
    }

    // ok
    const hoverStop = computed(
      () => props.hover && props.modelValue && isScroll.value
    )

    function reset() {
      cancle();
      isHover.value = false;
      initMove();
    }

    watch(
      () => props.list,
      () => {
        if (props.isWatch) {
          reset();
        }
      },
      {
        deep: true,
      }
    );

    watch(
      () => props.modelValue,
      (newValue) => {
        if (newValue) {
          startMove();
        } else {
          stopMove();
        }
      }
    );

    watch(() => props.count, (newValue) => {
      if (newValue !== 0) {
        startMove();
      }
    })

    onBeforeMount(() => {
      cancle();
      clearTimeout(singleWaitTimeout.value);
    });

    onMounted(() => {
      initMove()
    })
    
    const { default: $default, html } = slots
    const copyNum = new Array(props.copyNum).fill(null)
    return () => (
      <div ref={scrollRef} class={attrs.class}>
        <div ref={realBoxRef} style={realBoxStyle.value} onMouseenter={() => {
          if (hoverStop.value) {
            stopMove();
          }
        }} onMouseleave={() => {
          if (hoverStop.value) {
            startMove();
          }
        }}>
          <div ref={slotListRef} style={floatStyle.value}>
            {$default!()}
          </div>
          {
            isScroll ? copyNum.map(() => {
              if (html && typeof html === 'function') {
                return (<div style={floatStyle.value}>{html()}</div>)
              } else {
                return (<div style={floatStyle.value}>{$default!()}</div>)
              }
            }) : null
          }
        </div >
      </div >
    )
  }
})

export default Vue3SeamlessScroll

组件结构

首先看看在defineComponent中返回的部分:

// 通过结构赋值拿到slots的 default 和 html 属性
const { default: $default, html } = slots

以前我并没有使用过这个html属性,于是去查了一下slots的类型定义:

export declare type Slots = Readonly<InternalSlots>;

declare type InternalSlots = {
    [name: string]: Slot | undefined;
};
export declare type Slot = (...args: any[]) => VNode[];

可以看到slots是一个只读的对象,key为字符串,value为一个返回VNode数组的函数。这也是为什么我们可以很方便的使用匿名插槽、具名插槽等功能,默认情况下这里的name的值为default,通过slots.default!()便可以拿到匿名插槽中的VNode。

而html部分,是在父组件也为tsx文件时,向子组件传递进来了一个html结构的函数,可以通过判断html是否为function来进行渲染。

const copyNum = new Array(props.copyNum).fill(null)

props.copyNum的作用是声明待滚动列表需要拷贝的次数,fill方法是将数组中的所有元素都用null值代替,假设copyNum = 3,那么通过这个操作可以生成[null, null, null]数组。

平时使用的时候将文件设置为无限循环滚动的话就不必使用这个功能

return () => (
    <!-- attrs值为是父组件中没有通过bind绑定的数据 -->
    <div ref={scrollRef} class={attrs.class}>
        <!-- 作为滚动列表item的父容器,设置transition等动画样式 -->
        <div ref={realBoxRef} style={realBoxStyle.value} 
            <!-- 增加两个原生事件,负责判断鼠标悬停 -->
            onMouseenter={() => {
                if (hoverStop.value) {
                    stopMove();
                }
            }} 
            onMouseleave={() => {
                if (hoverStop.value) {
                    startMove();
                }
            }}
        >
        	<!-- 设置具体的子滚动item -->
            <div ref={slotListRef} style={floatStyle.value}>
                {$default!()}
            </div>
            <!-- 这里也是设置具体的子滚动item,设置的是拷贝列表的数据 -->
            {	
                isScroll ? copyNum.map(() => {
                    if (html && typeof html === 'function') {
                        return (<div style={floatStyle.value}>{html()}</div>)
                    } else {
                        return (<div style={floatStyle.value}>{$default!()}</div>)
                    }
                }) : null
            }
        </div>
    </div>
)

所以这个组件大致的html结构为:

|外部根容器
|
|————滚动item的父容器,设置动画样式
|
|————————滚动item

结构功能

结合组件结构往下介绍,首先从上向下介绍组件结构中出现过的方法

realBoxStyle

通过computed动态返回css对象形式的样式

const realBoxStyle = computed<CSSProperties>(() => {
	return {
        width: realBoxWidth.value ? `${realBoxWidth.value}px` : 'auto',
        transform: `translate(${xPos.value}px,${yPos.value}px)`,
        // @ts-ignore
        transition: `all ${typeof props.ease === 'string' ? props.ease : 'cubic-bezier(' + props.ease.x1 + ',' + props.ease.y1 + ',' + props.ease.x2 + ',' + props.ease.y2 + ')'} ${props.delay}ms`,
        overflow: 'hidden'
      }
    })

hoverStop

如果想要执行鼠标悬停的功能,首先需要确保props传递进来的hover为true。

所以如果开启了悬停功能,hoverStop的值将固定为true,这是为了确保后面的onMouseEnter事件顺利的恒定执行

const hoverStop = computed(
	() => props.hover && props.modelValue && isScroll.value
)

stopMove

暂停滚动效果的时候,就是鼠标悬停的时候,所以负责记录鼠标isHover的值将会变为true

同时,毕竟这是一个滚动的列表,所以需要清除定时器。之后会在具体的滚动方法中介绍设置定时器

function stopMove() {
	isHover.value = true;
	if (singleWaitTimeout.value) {
		clearTimeout(singleWaitTimeout.value);
	}
	cancle();
}

滚动

在进行滚动部分的代码编写之前,再次看看组件的结构。

具体滚动的是滚动item,所以在具体的执行滚动操作之前,我们需要先对他的父元素进行一些初始化。

initMove

function initMove() {
    // 设置滚动列表的宽度
	if (isHorizontal.value) {
        // 获取到每一个item的宽度
		let slotListWidth = slotListRef?.value?.offsetWidth;
		slotListWidth = slotListWidth! * 2 + 1;
		realBoxWidth.value = slotListWidth;
	}
	// 判断是否可以滚动 如果可以滚动,就执行内部的move方法
	if (isScroll.value) {
		realBoxHeight.value = realBoxRef?.value?.offsetHeight!;
		if (props.modelValue) {
			move();
		}
	} else {
		cancle();
		yPos.value = xPos.value = 0;
	}
}

由于可以通过props设置滚动的方向,所以在判断是否滚动之前,我们可以先判断一下它的具体滚动方向:

const isHorizontal = computed(() => 
	props.direction == 'left' || props.direction == 'right'
)

如果列表长度大于最小滚动长度,就可以发生滚动:

const isScroll = computed(() => props.list!.length >= props.limitScrollNum)

move

function move() {
    // 和使用定时器一样,在设置运动操作之前需要清除上一次的运动
	cancle();
    // 手动暂停,_count负责记录已经滚动过去了多少个item
    if (isHover.value || !isScroll.value || _count.value === props.count) {
        emit('stop', _count.value);
        _count.value = 0;
        return;
    }
    reqFrame.value = requestAnimationFrame(function () {
        const h = realBoxHeight.value / 2
        const w = realBoxWidth.value / 2
        let { direction, singleWaitTime } = props
        // 结合realBoxStop来看,transform(xPos,yPos),所以向上移动的话,yPos是 < 0的
        if (direction === 'up') {
            // 如果上移过多,就会回到原点,同时滚动次数+1
            if (Math.abs(yPos.value) >= h) {
                yPos.value = 0
                _count.value += 1
                emit('count', _count.value)
            }
            // 每次滚动单步距离
            yPos.value -= step.value
            // 下同
        } else if (direction === 'down') {
            if (yPos.value >= 0) {
                yPos.value = h * -1
                _count.value += 1
                emit('count', _count.value)
            }
            yPos.value += step.value
        } else if (direction === 'left') {
            if (Math.abs(xPos.value) >= w) {
                xPos.value = 0
                _count.value += 1
                emit('count', _count.value)
            }
            xPos.value -= step.value
        } else if (direction === 'right') {
            if (xPos.value >= 0) {
                xPos.value = w * -1
                _count.value += 1
                emit('count', _count.value)
            }
            xPos.value += step.value
        }
        if (singleWaitTimeout.value) {
            clearTimeout(singleWaitTimeout.value)
        }
        // !!的作用就是将所有的值转换为boolean类型
        // !!1 = true	!!0 = false	  !!null = false	!!'' = false
        if (!!realSingleStopHeight.value) {
            if (Math.abs(yPos.value) % realSingleStopHeight.value < step.value) {
                singleWaitTimeout.value = setTimeout(() => {
                    move()
                }, singleWaitTime)
            } else {
                move()
            }
        } else if (!!realSingleStopWidth.value) {
            if (Math.abs(xPos.value) % realSingleStopWidth.value < step.value) {
                singleWaitTimeout.value = setTimeout(() => {
                    move()
                }, singleWaitTime)
            } else {
                move()
            }
        } else {
            move()
        }
    });
}

数据刷新

在做完基本滚动功能后,可以考虑一下数据处理的问题

比如这里,我们可以监测list数值的变化,同时开启deep: true开启深度监测

function reset() {
	cancle();
	isHover.value = false;
	initMove();
}

watch(
	() => props.list,
	() => {
		if (props.isWatch) {
			reset();
		}
	},
	{
		deep: true,
	}
);
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-17 22:02:04  更:2022-03-17 22:05:04 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 5:43:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码