我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
起源
是因为看到,实时信息的滚动播放。我就在思考,这是怎么实现的呢?
思路分析
- HTML结构,有一个存放列表的容器。
- 滚动。支持上下左右的滚动。
- 步长。
- hover停止。
- 无缝滚动。
实现
这里使用了vue-seamless-scroll库。快速实现列表无缝滚动。
源码解读
- requestAnimationFrame 实现动画的API
requestAnimationFrame是浏览器提供用来绘制动画一个api。它会在浏览器的每一帧Layout,Paint之前执行,就是常说的回流和重绘之前执行,这样可以很方便的执行一段更改样式的代码,然后浏览器就能马上回流,重绘,生成一帧。 Window.requestAnimationFrame()
优势:
CPU节能:
使用SetTinterval 实现的动画,当页面被隐藏或最小化时,SetTinterval 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。
而RequestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统走的RequestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。
函数节流:
在高频率事件( resize, scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,RequestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销,一个刷新间隔内函数执行多次时没有意义的,因为多数显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。
减少DOM操作:
requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
setTimeout执行动画的缺点:
它通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象;原因是: settimeout任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;
settimeout的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧。
-
滚动方向 滚动的方向是通过css样式来控制。
pos () { return { transform: `translate(${this.xPos}px,${this.yPos}px)`, transition: `all ${this.ease} ${this.delay}ms`, overflow: 'hidden' } }, -
无缝连接
- 通过copyHtml的方式,复制一个相同的节点模块,接在dom节点之后。这样每次循环的时候,就是无缝的了。这里的思想和图片无缝轮播是一样的。无缝轮播的实现思路
<div ref="realBox" :style="pos" @mouseenter="enter" @mouseleave="leave" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" > <div ref="slotList" :style="float"> <slot></slot> </div> <div v-html="copyHtml" :style="float"></div> </div>_initMove () { this.$nextTick(() => { const { switchDelay } = this.options const { autoPlay, isHorizontal } = this this._dataWarm(this.data) this.copyHtml = '' //清空copy if (isHorizontal) { this.height = this.$refs.wrap.offsetHeight this.width = this.$refs.wrap.offsetWidth let slotListWidth = this.$refs.slotList.offsetWidth // 水平滚动设置warp width if (autoPlay) { // 修正offsetWidth四舍五入 slotListWidth = slotListWidth * 2 + 1 } this.$refs.realBox.style.width = slotListWidth + 'px' this.realBoxWidth = slotListWidth } if (autoPlay) { this.ease = 'ease-in' this.delay = 0 } else { this.ease = 'linear' this.delay = switchDelay return } // 是否可以滚动判断 if (this.scrollSwitch) { let timer if (timer) clearTimeout(timer) this.copyHtml = this.$refs.slotList.innerHTML setTimeout(() => { this.realBoxHeight = this.$refs.realBox.offsetHeight this._move() }, 0); } else { this._cancle() this.yPos = this.xPos = 0 } }) }, -
运行起来
mounted () { this._initMove()// 在调用 _move() },_move () { // 鼠标移入时拦截_move() if (this.isHover) return this._cancle() //进入move立即先清除动画 防止频繁touchMove导致多动画同时进行 this.reqFrame = requestAnimationFrame( function () { const h = this.realBoxHeight / 2 //实际高度 const w = this.realBoxWidth / 2 //宽度 let { direction, waitTime } = this.options let { step } = this if (direction === 1) { // 上 if (Math.abs(this.yPos) >= h) { // 重点: 这里基于高度做判断处理,如果第一个div,向上移动完成之后,条件成立,y值重置为0; this.$emit('ScrollEnd') this.yPos = 0 } this.yPos -= step } else if (direction === 0) { // 下 if (this.yPos >= 0) { this.$emit('ScrollEnd') this.yPos = h * -1 } this.yPos += step } else if (direction === 2) { // 左 if (Math.abs(this.xPos) >= w) { this.$emit('ScrollEnd') this.xPos = 0 } this.xPos -= step } else if (direction === 3) { // 右 if (this.xPos >= 0) { this.$emit('ScrollEnd') this.xPos = w * -1 } this.xPos += step } if (this.singleWaitTime) clearTimeout(this.singleWaitTime) if (!!this.realSingleStopHeight) { //是否启动了单行暂停配置 if (Math.abs(this.yPos) % this.realSingleStopHeight < step) { // 符合条件暂停waitTime this.singleWaitTime = setTimeout(() => { this._move() }, waitTime) } else { this._move() } } else if (!!this.realSingleStopWidth) { if (Math.abs(this.xPos) % this.realSingleStopWidth < step) { // 符合条件暂停waitTime this.singleWaitTime = setTimeout(() => { this._move() }, waitTime) } else { this._move() } } else { this._move() } }.bind(this) ) },
运行起来
- mounted中调用this._initMove()。_initMove中,进行了节点的复制,解决无缝连接;计算容器的高度记录。
- 在调用 _move()。进行高度的判断,进行x或者y值得计算,设置transform,translate,x,y值得修改;transition的动画设置;requestAnimationFrame()进行调动;进行循环调用this._move();
- 通过计算属性,更新css样式。:style="pos"
vue-seamless-scroll
使用指南。
注意项
1.最外层容器需要手动设置width、height、overflow:hidden
2.左右的无缝滚动需要给主内容区域(即默认slot插槽提供)设定合适的css width属性(否则无法正确计算实际宽度)。 也可以通过给他设置为display:flex;无需设置css width属性
3.step值不建议太小,不然会有卡顿效果(如果设置了单步滚动,step需是单步大小的约数,否则无法保证单步滚动结束的位置是否准确。~~~~~,比如单步设置的30,step不能为4)
4.需要实现手动切换左右滚动的时候,必须设置autoPlay:false(1.1.17版本开始,只需要设置navigation:false),目前不支持环路
5.提供了slot left-switch || right-switch可以自由定义需要的按钮样式 外层有div已经定位了位置居中,距离两边侧的距离可以通过switchOffset参数调整
6.当按钮到达边界位置,会自动为无法点击状态按钮加上定义的switchDisabledClass: 'disabled',可以按需配置