Skip to content Skip to footer

记录---实现抖音 “视频无限滑动“效果

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

前言

在家没事的时候刷抖音玩,抖音首页的视频怎么刷也刷不完,经常不知不觉的一刷就到半夜了😅

不禁感叹道 "垃圾抖音,费我时间,毁我青春😅"

最终效果

在线预览:dy.ttentau.top/Github地址:github.com/zyronon/dou…源码:SlideVerticalInfinite.vue

实现原理

无限滑动的原理和虚拟滚动的原理差不多,要保持 SlideList 里面永远只有 N 个 SlideItem,就要在滑动时不断的删除和增加 SlideItem。

滑动时调整 SlideList 的偏移量 translateY 的值,以及列表里那几个 SlideItem 的 top 值,就可以了

为什么要调整 SlideList 的偏移量 translateY 的值同时还要调整 SlideItem 的 top 值呢?

因为 translateY 只是将整个列表移动,如果我们列表里面的元素是固定的,不会变多和减少,那么没关系,只调整 translateY 值就可以了,上滑了几页就减几页的高度,下滑同理

但是如果整个列表向前移动了一页,同时前面的 SlideItem 也少了一个,,那么最终效果就是移动了两页…因为 塌陷 了一页

这显然不是我们想要的,所以我们还需要同时调整 SlideItem 的 top 值,加上前面少的 SlideItem 的高度,这样才能显示出正常的内容

步骤

定义

virtualTotal:页面中同时存在多少个 SlideItem,默认为 5。

//页面中同时存在多少个SlideItem

virtualTotal: {

type: Number,

default: () => 5

},

设置这个值可以让外部组件使用时传入,毕竟每个人的需求不同,有的要求同时存在 10 条,有的要求同时存在 5 条即可。

不过同时存在的数量越大,使用体验就越好,即使用户快速滑动,我们依然有时间处理。

如果只同时存在 5 条,用户只需要快速滑动两次就到底了(因为屏幕中显示第 3 条,刚开始除外),我们可能来不及添加新的视频到最后

render:渲染函数,SlideItem内显示什么由render返回值决定

render: {

type: Function,

default: () => {

return null

}

},

之所以要设定这个值,是因为抖音首页可不只有视频,还有图集、推荐用户、广告等内容,所以我们不能写死显示视频。

最好是定义一个方法,外部去实现,我们内部去调用,拿到返回值,添加到 SlideList 中

list:数据列表,外部传入

list: {

type: Array,

default: () => {

return []

}

},

我们从 list 中取出数据,然后调用并传给 render 函数,将其返回值插入到 SlideList中

初始化

watch(

() => props.list,

(newVal, oldVal) => {

//新数据长度比老数据长度小,说明是刷新

if (newVal.length < oldVal.length) {

//从list中取出数据,然后调用并传给render函数,将其返回值插入到SlideList中

insertContent()

} else {

//没数据就直接插入

if (oldVal.length === 0) {

insertContent()

} else {

// 走到这里,说明是通过接口加载了下一页的数据,

// 为了在用户快速滑动时,无需频繁等待请求接口加载数据,给用户更好的使用体验

// 这里额外加载3条数据。所以此刻,html里面有原本的5个加新增的3个,一共8个dom

// 用户往下滑动时只删除前面多余的dom,等滑动到临界值(virtualTotal/2+1)时,再去执行新增逻辑

}

}

}

)

用 watch 监听 list 是因为它一开始不一定有值,通过接口请求之后才有值

同时当我们下滑 加载更多 时,也会触发接口请求新的数据,用 watch 可以在有新数据时,多添加几条到 SlideList 的最后面,这样用户快速滑动也不怕了

如何滑动

这里就不再赘述,参考我的这篇文章:200行代码实现类似Swiper.js的轮播组件

滑动结束

判断滑动的方向

当我们向上滑动时,需要删除最前面的 dom ,然后在最后面添加一个 dom

下滑时反之

slideTouchEnd(e, state, canNext, (isNext) => {

if (props.list.length > props.virtualTotal) {

//手指往上滑(即列表展示下一条视频)

if (isNext) {

//删除最前面的 `dom` ,然后在最后面添加一个 `dom`

} else {

//删除最后面的 `dom` ,然后在最前面添加一个 `dom`

}

}

})

手指往上滑(即列表展示下一条视频)

首先判断是否要加载更多,快到列表末尾时就要加载更多数据了再判断是否符合 腾挪 的条件,即当前位置要大于 half,且小于列表长度减 half。在最后面添加一个 dom删除最前面的 dom将所有 dom 设置为最新的 top 值(原因前面有讲,因为删除了最前面的 dom,导致塌陷一页,所以要加上删除 dom 的高度)

let half = (props.virtualTotal - 1) / 2

//删除最前面的 `dom` ,然后在最后面添加一个 `dom`

if (state.localIndex > props.list.length - props.virtualTotal && state.localIndex > half) {

emit('loadMore')

}

//是否符合 `腾挪` 的条件

if (state.localIndex > half && state.localIndex < props.list.length - half) {

//在最后面添加一个 `dom`

let addItemIndex = state.localIndex + half

let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addItemIndex}']`)

if (!res) {

slideListEl.value.appendChild(getInsEl(props.list[addItemIndex], addItemIndex))

}

//删除最前面的 `dom`

let index = slideListEl.value

.querySelector(`.${itemClassName}:first-child`)

.getAttribute('data-index')

appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {

_css(item, 'top', (state.localIndex - half) * state.wrapper.height)

})

}

手指往下滑(即列表展示上一条视频)

逻辑和上滑都差不多,不过是反着来而已

再判断是否符合 腾挪 的条件,和上面反着在最前面添加一个 dom删除最后面的 dom将所有 dom 设置为最新的 top 值

//删除最后面的 `dom` ,然后在最前面添加一个 `dom`

if (state.localIndex >= half && state.localIndex < props.list.length - (half + 1)) {

let addIndex = state.localIndex - half

if (addIndex >= 0) {

let res = slideListEl.value.querySelector(`.${itemClassName}[data-index='${addIndex}']`)

if (!res) {

slideListEl.value.prepend(getInsEl(props.list[addIndex], addIndex))

}

}

let index = slideListEl.value

.querySelector(`.${itemClassName}:last-child`)

.getAttribute('data-index')

appInsMap.get(Number(index)).unmount()

slideListEl.value.querySelectorAll(`.${itemClassName}`).forEach((item) => {

_css(item, 'top', (state.localIndex - half) * state.wrapper.height)

})

}

其他问题

为什么不直接用 v-for直接生成 SlideItem 呢?

如果内容不是视频就可以。要删除或者新增时,直接操作 list 数据源,这样省事多了

如果内容是视频,修改 list 时,Vue 会快速的替换 dom,正在播放的视频,突然一下从头开始播放了😅😅😅

如何获取 Vue 组件的最终 dom

有两种方式,各有利弊

用 Vue 的 render 方法

优点:只是渲染一个 VNode 而已,理论上讲内存消耗更少。

缺点:但我在开发中,用了这个方法,任何修改都会刷新页面,有点难蚌😅

用 Vue 的 createApp 方法再创建一个 Vue 的实例

和上面相反😅

import { createApp, onMounted, reactive, ref, render as vueRender, watch } from 'vue'

/**

* 获取Vue组件渲染之后的dom元素

* @param item

* @param index

* @param play

*/

function getInsEl(item, index, play = false) {

// console.log('index', cloneDeep(item), index, play)

let slideVNode = props.render(item, index, play, props.uniqueId)

const parent = document.createElement('div')

//TODO 打包到线上时用这个,这个在开发时任何修改都会刷新页面

if (import.meta.env.PROD) {

parent.classList.add('slide-item')

parent.setAttribute('data-index', index)

//将Vue组件渲染到一个div上

vueRender(slideVNode, parent)

appInsMap.set(index, {

unmount: () => {

vueRender(null, parent)

parent.remove()

}

})

return parent

} else {

//创建一个新的Vue实例,并挂载到一个div上

const app = createApp({

render() {

return {slideVNode}

}

})

const ins = app.mount(parent)

appInsMap.set(index, app)

return ins.$el

}

}

总结

原理其实并不难。主要是一开始可能会用 v-for 去弄,折腾半天发现不行。v-for 不行,就只能想想怎么把 Vue 组件搞到 html 里面去,又去研究如何获取 Vue 组件的最终 dom,又查了半天资料,Vue 官方文档也不写,还得去翻 api ,麻了

本文转载于:https://juejin.cn/post/7361614921519054883

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

Copyright © 2088 上届世界杯冠军_u20世界杯八强 - longxinwl.com All Rights Reserved.
友情链接