当前位置: 首页 > Web前端 > vue.js

一个业务需求实现四个轮播组件的思路

时间:2023-04-01 00:17:43 vue.js

需求原型假设有一列未知长度的数据,想在一个容器中展示轮播。基本结构如下

    item
我们需要实现的是组件实现思路1.我们使用css3的translateX进行平移滑动,在列表移出容器的瞬间,重置到容器右侧不可见的地方。第一步是完成布局,确定元器件的基本结构。
设置基本样式结构。旋转木马{位置:相对;显示:弹性;对齐项目:居中;&-wrap{溢出:隐藏;位置:相对;显示:弹性;弹性:1;对齐项目:居中;高度:100%;}&-content{transition-timing-function:linear;}}这样就完成了整体布局。您可以查看上面的视图了解详细信息。下一个问题是如何让元素移动。第二步设置动画,设置基本参数conststate=reactive({offset:0,duration:0})mainanimationimplementation//stylecontrolconstgetStyle=(data)=>;{返回{转换:data.offset?`translateX(${data.offset}px)`:'',transitionDuration:`${data.duration}s`,}}//轮播样式控制conststyle=computed(()=>getStyle(state))第三步是计算动画逻辑。首先需要计算具体的元素获取参数//containerwidthletwrapWidth=0//contentwidthletcontentWidth=0letstartTime=nullconstwrapRef=ref(null)constcontentRef=ref(null)//之后元素已安装,计算逻辑开始。onMounted(reset)通常暴露给外部选项constprops=defineProps({show:{type:Boolean,default:true,},speed:{type:[Number,String],default:30,},delay:{type:[Number,String],default:0,},})初始挂载计算逻辑constreset=()=>{//重置参数wrapWidth=0contentWidth=0state.offset=0state.duration=0clearTimeout(startTime)startTime=setTimeout(()=>{//拦截DOM未渲染阶段if(!wrapRef.value||!contentRef.value)returnconstwrapRefWidth=useRect(wrapRef).widthconstcontentRefWidth=useRect(contentRef).width//如果内容宽度超过容器宽度则运行if(contentRefWidth>wrapRefWidth){wrapWidth=wrapRefWidth;contentWidth=contentRefWidth;//calldoubleRaf(()=>{state.offset=-contentWidth/2;state.duration=-state.offset/+props.speed;})}},props.delay);}这段代码其实有几个要考虑的要点。doubleRaf函数的作用是什么?这是完整的功能代码。可以看到它只是简单的执行了回调exportfunctionraf(fn){returnrequestAnimationFrame(fn)}exportfunctiondoubleRaf(fn){raf(()=>raf(fn));}让我们回顾一下requestAnimationFrame做了什么window.requestAnimationFrame()告诉浏览器——你要执行一个Animation,并要求浏览器在下一次重绘更新动画之前调用指定的回调函数回调函数执行次数通常为每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数的执行次数通常与浏览器屏幕刷新次数匹配。为了提高性能和电池寿命,在大多数浏览器中,当requestAnimationFrame()在后台选项卡或隐藏的iframe中运行时,requestAnimationFrame()将被挂起以提高性能和电池寿命。一般情况下会根据浏览器提供最佳最快的执行时机,在隐身运行模式下会临时自动调用。这时候就会出现第二个问题:为什么要等到第二次重绘才执行回调呢?用法其实是我在研究vantUI库源码的时候看到的。他们的代码注释是这样写的//使用doubleraf确保动画可以启动PCemulator试过运行它,下次重绘时直接执行回调是可以的,但是实际运行时会有影响写入的因素。假设在最简单的模式下,我们希望一个元素的位移是translateX(0)->translateX(1000px)->translateX(500px)如果代码如下box.style.transform='translateX(1000px)'requestAnimationFrame(()=>{box.style.transform='translateX(500px)'})它的执行顺序会是,具体原因大家可以回忆一下requestAnimationFrame的作用translateX(0)->translateX(500px)既然你知道问题了现在,答案就出来了。第四步是重复上面图2所示的动画。当动画结束时,它被重置到容器右侧的一个不可见位置。首先,我们整个动画是用CSS3实现的,偏移值是直接设置的。然后通过过渡效果创建动画constgetStyle=(data)=>{return{transform:data.offset?`translateX(${data.offset}px)`:'',transitionDuration:`${data.duration}s`,}}现在我们知道CSS3过渡动画实现了,我们可以使用transitionend监听transitionend事件并在CSS完成转换后触发它。注意:如果转换在完成之前被删除,例如CSStransition-property属性被移除,过渡事件将不会被触发。constonTransitionEnd=()=>{state.offset=wrapWidthstate.duration=0raf(()=>{//使用双raf确保动画可以开始doubleRaf(()=>{state.offset=-contentWidth;state.duration=(contentWidth+wrapWidth)/+props.speed;});});}可以看到过滤结束后,会立即重置属性,然后更新状态。原理,它的主要关键步骤是在读取一个值时进行跟踪:代理的get处理函数中的track函数记录了属性和当前的副作用。检测值何时更改:调用代理上的设置处理程序。重新运行代码以读取原始值:触发器函数查找哪些副作用取决于该属性并执行它们。组件的模板被编译成渲染函数。渲染函数创建描述组件应该如何渲染的VNodes。它包含在一个副作用中,允许Vue在运行时跟踪被“触摸”的属性。渲染函数在概念上与计算属性非常相似。Vue不会准确跟踪依赖项是如何使用的,它只知道它们在函数运行的某个时刻被使用过。如果随后更改了这些属性中的任何一个,它将触发副作用再次运行,重新运行渲染函数以生成新的VNode。然后使用这些移动对DOM进行必要的修改。Vue在更新DOM时异步执行。当数据发生变化时,Vue会开启一个异步更新队列。视图需要等待队列中的所有数据变化完成后,再统一更新。所以理论上,考虑到性能损失,我们应该更新下一个队列中的动画。Vue提供了一个全局APInextTick,该API会将回调延迟到下一个DOM更新周期之后。更改一些数据后立即使用它以等待DOM更新。为什么不用nextTick再嵌套一层raf呢?vant源代码中有相关注释//等待Vue渲染偏移量//使用nextTick在iOS14中不起作用。整个代码实现思路已经完成,但是这种写法会有一个明显的缺点。整个动画只能融进融出。界面中间会有一段空白的元素等待动画进入场景,所以需要向下展开。考虑如何实现无缝轮播的效果。实现思路2我们直接复制两份相同的元素,然后使用延迟执行来达到无缝衔接的效果。第一步完成布局
改为绝对定位。旋转木马{位置:相对;显示:弹性;对齐项目:居中;&-wrap{溢出:隐藏;位置:相对;显示:弹性;弹性:1;对齐项目:居中;高度:100%;}&-content{位置:绝对;显示:弹性;空白:nowrap;转换时间函数:线性;}}第二步是设置动画,把idea1的基本变量复制两次,所以代码就省略了。第三步是计算动画逻辑。是关键代码核心constreset=()=>{...省略...//如果内容宽度超过容器宽度则运行if(contentRefWidth>wrapRefWidth){wrapWidth=wrapRefWidthcontentWidth=contentRefWidth;//实际的动画属性是一样的constoffset=-(contentWidth+wrapWidth)constduration=-offset/+props.speed//Element1animationdoubleRaf(()=>{state1.offset=offset;state1.duration=duration;})//元素2动画setTimeout(()=>{doubleRaf(()=>{state2.offset=offset;state2.duration=duration;})},contentWidth/+props.speed*1000);//开始向元素1后面滑动}...省略...}偏移值的计算方法是因为从容器的右侧到容器的左侧实际上等于容器的宽度+宽度元素的,知道速度不变,就可以知道offsetdurationconstoffset=-(contentWidth+wrapWidth)constduration=-offset/+props.speed延迟计算方法为了达到无缝衔接的效果,元素2需要在元素1不重叠的时刻开始滑动,即元素偏移距离等于自身宽度时contentWidth/+props.speed*1000第四步,重复动画这时候,其实可以发现元素1和元素2在重复执行同一个动画,所以它们共享同一个事件constonTransitionEnd=()=>{state.offset=0state.duration=0raf(()=>{doubleRaf(()=>{state.offset=-(contentWidth+wrapWidth);state.duration=-state.offset/+props.speed;});});}不足这种写法是简单的复制元素拼接实现依次滑动的效果,但是有一些明显的缺点代码和视觉效果不一定统一。如果元素内的布局间隔不相等或不对称,很明显两个不同的元素被合并了。其实这是一个无法回避的问题。我们只能标准化元素样式计算值的偏差,因为它涉及到元素的像素、偏移位置、过渡时间和定时器。一些带小数的数值计算会造成明显的不同步,表现为滑动速度不同步,元素间间隔过大或部分重合等问题,第二点对暂且如此,后面放弃这种写法,去实现第三个想法。其实和第二种思路类似,只是没有使用绝对定位,使用的是自然偏移。一开始,元素已经出现在视图中。第一步是完成布局。不需要绝对定位。第二步设置动画复制第一个idea的基本变量两次,所以省略第三步代码,计算动画逻辑。这是关键代码核心。constreset=()=>{//重置参数wrapWidth=0contentWidth=0state1.offset=0state1.duration=0state2.offset=0state2.duration=0clearTimeout(startTime)startTime=setTimeout(()=>{//拦截DOM未渲染阶段if(!wrapRef.value||!contentRef.value)returnconstwrapRefWidth=useRect(wrapRef).widthconstcontentRefWidth=useRect(contentRef).width//仅在内容宽度超过容器时运行宽度if(contentRefWidth>wrapRefWidth){wrapWidth=wrapRefWidthcontentRefWidth=condRaft(()=>{state1.offset=-contentWidth;state1.duration=-state1.offset/+props.speed;state2.offset=-contentWidth*2;state2.duration=-state2.offset/+props.speed;})}},props.delay);}元素1的偏移量计算因为已经在view中了,所以只需要偏移自己的widthstate1.offset=-contentWidth;state1.duration=-state1.offset/+props.speed;元素2的偏移量计算因为是接在元素1的后面,所以初始偏移距离等于两者之和state2.offset=-contentWidth*2;state2.duration=-state2.offset/+props.speed;第四步,重复动画和思路和第三个相比,因为不是使用绝对定位,重置的时候需要计算偏移值,两个元素也需要分别计算,因为元素1的初始偏移值为0,但是重置后需要定位到元素2的初始位置,所以constonTransitionEnd1=()=>{state1.offset=contentWidth*2-wrapWidthstate1.duration=0raf(()=>{doubleRaf(()=>{state1.offset=-contentWidth*2;state1.duration=-state1.offset/+props.speed;});});}元素2的初始位置不需要改变,也不需要偏移值constonTransitionEnd2=()=>{state2.offset=0state2.duration=0raf(()=>{doubleRaf(()=>{state2.offset=-contentWidth*2;state2.duration=-state2.offset/+props.speed;});});}不够还是有思路2的问题,所以我也放弃了思路4的实现,这是一种不太严谨的写法,但是vue3-seamless-scroll里面用了同样的方法直接处理两个元素作为一个整体,当偏移值达到自身宽度的一半时,回滚第一步完成布局第二步是设置动画,将idea1的基本变量复制两次,所以省略第三步的代码,计算动画逻辑这是关键代码核心,因为元素宽度是合并计算的,所以偏移值需要除以2constreset=()=>{...省略...doubleRaf(()=>{state.offset=-contentWidth/2;state.duration=-state.offset/+props.speed;}...省略...}第四步,重复动画一直重复,不用改合作社nstonTransitionEnd=()=>{state.offset=0state.duration=0raf(()=>{doubleRaf(()=>{state.offset=-contentWidth/2;state.duration=-state.offset/+props.speed;});});}不足因为一直是自己偏移,没有上面2个偏差的问题,但是仔细看reset的瞬间会有轻微的停顿,除此之外很流畅,所以最后使用这个方案实现了一个Carousel组件