美文网首页
滚动组件

滚动组件

作者: sweetBoy_9126 | 来源:发表于2019-08-03 18:21 被阅读0次

前置知识:
基于我们的表格table组件如果在数据量很多的情况下,比如有10000条数据的时候那么我们页面渲染到底有多慢

let array = [
  {name: '立发', score: 100, description: 'xxx'},
  {name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
{name: '立发', score: 100, description: 'xxx'},
]
let bigArr = []
for (let i = 0; i< 1000; i++) {
  bigArr.push(...array.map(item, index) => {
    return {id: i*20+index, ...item}
    })
},
data () {
  dataSource: bigArr
}

点击performance录制,渲染完成点击stop

黄色的是js是在计算,紫色的是dom在渲染,绿色是在绘制,蓝色在下载
上面从第2秒的时候就开始计算一直到第8秒的时候还在计算,也就是说维持了6秒,而当我们把10000项数据改成1000的时候绘制图如下

明显的快了特别多

解决方法
只显示用户看的见得数据量,比如说一万项,那我们只展示100项,当用户往下滚动了10项数据的高度的时候上面的十项隐藏,下面的十项显示

针对上面只显示用户看的见得数据的需求,所以我们需要自己实现一个滚动条组件,因为默认的滚动条只有上面有内容才会显示,而我们即使上面的数据是空的也是显示

浏览器的默认的滚动条出现的条件

  1. 父元素内的子元素高度大于父元素
    2.父元素设置css为overflow:auto
1.实现用户往下滑内容上移,往上滑内容下移

首先我们需要把父元素的滚动条干掉,设置overflow:hidden
这里要注意因为我们把滚动条禁用了,所以我们不能监听滚动事件了,我们需要监听wheel事件

<div class="parent" ref="parent">
      <div class="child" ref="child">
        <p>1</p>
        <p>2</p>
        <p>3</p>
        <p>4</p>
        <p>5</p>
         ...
      <p>100</p>
    </div>
</div>

mounted() {
   let parent = this.$refs.parent
   let child = this.$refs.child
   parent.addEventListener('wheel', () => {
       console.log('wheel')
   })
}
.page {
    display: flex;
    justify-content: center;
  }

  .parent {
    height: 400px;
    overflow: hidden;
    width: 400px;
    border: 5px solid red;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
  }

  .child {
    border: 5px solid green;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
  }

通过deltaY拿到垂直方向的滚动距离,配合trasform设置y轴偏移

mounted() {
              let parent = this.$refs.parent
              let child = this.$refs.child
              child.style.transition = `transform 1s linear`
              let translateY = 0
              parent.addEventListener('wheel', (e) => {
                  if (e.deltaY > 0) {
                      translateY -= 10
                  } else if (e.deltaY === 0) {
                      translateY = translateY
                  } else {
                      translateY += 10
                      console.log('想看上面')
                  }
                  child.style.transform = `translateY(${translateY}px)`
              })
          }

优化滚动体验

上面的滚动条我们需要做三个优化

  1. 用户移的少,滚动的应该也少
  2. 从起点到结束的位置应该先慢再快再慢
  3. 不能超出起点和终点的位置
    上面的代码我们每次都把translateY减10或者加10,其实我们可以直接都减deltaY就可以,因为他向下是正的,向上是负的,然后我们对它乘以10来达到当我们鼠标滑动停止的时候他最短时间移动到对应的距离停下
child.style.transition = `transform .1s linear`
              let translateY = 0
              parent.addEventListener('wheel', (e) => {
                  if (e.deltaY > 0) {
                      translateY -= e.deltaY * 10
                      child.style.transform = `translateY(${translateY}px)`
                  } else if (e.deltaY === 0) {
                      console.log('没动')
                  } else {
                      translateY -= e.deltaY * 10
                      child.style.transform = `translateY(${translateY}px)`
                  }

              })
添加动画锁,来控制每次滚动过程中执行的滚动次数
let animationed = false
parent.addEventListener('wheel', (e) => {
  if (animationed) return
  if (e.deltaY > 0) {
      translateY -= e.deltaY * 10
      child.style.transform = `translateY(${translateY}px)`
      // 用来限制每次滚动只执行一次
      animationed = true
  } else if (e.deltaY === 0) {
      console.log('没动')
  } else {
      translateY -= e.deltaY * 10
      child.style.transform = `translateY(${translateY}px)`
      animationed = true
  }
})
child.addEventListener('transitionend', () => {
  // 过渡时间到了就置为false
  animationed = false
})

这样每一次滚动还是会一顿一顿的

再次优化

因为我们上面加了开关来控制滚动的限制,所以就会比较卡顿,这里我们去掉开关

  1. 对我们所有移动的距离乘以3,我们滚动10像素,文档就移动30像素,以达到我们滚动结束文档尽可能的第一时间停下来
translateY -= e.deltaY * 3
  1. 把子元素的transition的改为ease,用来让动画平滑一些
child.style.transition = `transform .1s ease`
  1. 如果我们滚动的太快了,用户根本就看不清我们的内容,所以我们要对每次滚动的最大距离做限制,我们这里以60为主,因为我们是乘以3,所以就是大于20的话就让他等于20
mounted() {
  let parent = this.$refs.parent
  let child = this.$refs.child
  child.style.transition = `transform .1s ease`
  let translateY = 0
  parent.addEventListener('wheel', (e) => {
    if (e.deltaY > 20) {
        translateY -= 20 * 3
    } else if (e.deltaY < -20) {
        translateY -= -20 * 3
    } else {
        translateY -= e.deltaY * 3
    }
    child.style.transform = `translateY(${translateY}px)`
  })
  1. 对顶部和底部进行限制
mounted() {
      let parent = this.$refs.parent
      let child = this.$refs.child
      child.style.transition = `transform .1s ease`
      let translateY = 0
      // 将height命名为childHeight
      let {height: childHeight} = child.getBoundingClientRect()
      let {height: parentHeight} = parent.getBoundingClientRect()
      let {borderTopWidth, borderBottomWidth, paddingTop, paddingBottom} = window.getComputedStyle(parent)
      borderTopWidth = parseInt(borderTopWidth)
      borderBottomWidth = parseInt(borderBottomWidth)
      paddingTop = parseInt(paddingTop)
      paddingBottom = parseInt(paddingBottom)
      let maxHeight = childHeight - parentHeight + (borderTopWidth+borderBottomWidth+paddingTop+paddingBottom)
      parent.addEventListener('wheel', (e) => {
        if (e.deltaY > 20) {
            translateY -= 20 * 3
        } else if (e.deltaY < -20) {
            translateY -= -20 * 3
        } else {
            translateY -= e.deltaY * 3
        }
        if (translateY > 0) {
          translateY = 0
        } else if (translateY < -maxHeight) {
          translateY = -maxHeight
        }
        child.style.transform = `translateY(${translateY}px)`
      })
    }
创建scroll.vue
  • demo.vue
<lf-scroll style="width: 400px;height: 400px">
  <p>1</p>
  <p>2</p>
  ...
</lf-scroll>
  • scroll.vue
<template>
  <div class="lifa-scroll-wrapper" ref="parent">
    <div class="lifa-scroll" ref="child">
      <slot></slot>
    </div>
  </div>
</template>

<script>
  export default {
    name: "LiFaUiScroll",
    mounted() {
      let parent = this.$refs.parent
      let child = this.$refs.child
      child.style.transition = `transform .1s ease`
      let translateY = 0
      // 将height命名为childHeight
      let {height: childHeight} = child.getBoundingClientRect()
      let {height: parentHeight} = parent.getBoundingClientRect()
      let {borderTopWidth, borderBottomWidth, paddingTop, paddingBottom} = window.getComputedStyle(parent)
      borderTopWidth = parseInt(borderTopWidth)
      borderBottomWidth = parseInt(borderBottomWidth)
      paddingTop = parseInt(paddingTop)
      paddingBottom = parseInt(paddingBottom)
      let maxHeight = childHeight - parentHeight + (borderTopWidth+borderBottomWidth+paddingTop+paddingBottom)
      parent.addEventListener('wheel', (e) => {
        if (e.deltaY > 20) {
          translateY -= 20 * 3
        } else if (e.deltaY < -20) {
          translateY -= -20 * 3
        } else {
          translateY -= e.deltaY * 3
        }
        if (translateY > 0) {
          translateY = 0
        } else if (translateY < -maxHeight) {
          translateY = -maxHeight
        }
        child.style.transform = `translateY(${translateY}px)`
      })
    }
  }
</script>

<style scoped lang="scss">
  .lifa-scroll {
    &-wrapper {
      border: 1px solid green;
      overflow: hidden;
    }
  }
</style>

问题:当我们滚动的时候会发现页面也会跟着抖动
解决方法:在没有到顶部或者底部的时候禁止它的默认行为

if (translateY > 0) {
  translateY = 0
} else if (translateY < -maxHeight) {
  translateY = -maxHeight
} else {
  e.preventDefault()
}
  1. 实现滚动条
    滚动条高度的计算如下图

也就是父元素的高度/子元素的高度=滚动条的高度/父元素的高度

<div class="lifa-scroll-wrapper" ref="parent">
    <div class="lifa-scroll" ref="child">
      <slot></slot>
    </div>
    <div class="lifa-scroll-track">
      <div class="lifa-scroll-bar" ref="scrollBar" :style="{transform: `translateY(${scrollMoveY})`}">
        <div class="lifa-scroll-bar-inner"></div>
      </div>
    </div>
  </div>

mounted() {
      this.updateScrollHeight(parentHeight, childHeight,)
    },
    methods: {
      updateScrollHeight(parentHeight, childHeight) {
        let bar = this.$refs.scrollBar
        bar.style.height = (parentHeight * parentHeight / childHeight) + 'px'
      }
    }

<style scoped lang="scss">
  .lifa-scroll {
    transition: all .05s ease;
    &-wrapper {
      border: 1px solid green;
      overflow: hidden;
      position: relative;
    }
    &-track {
      position: absolute;
      top: 0;
      right: 0;
      width: 14px;
      height: 100%;
      background: #FAFAFA;
      border-left: 1px solid #E8E7E8;
    }
    &-bar {
      position: absolute;
      top: -1px;
      left: 50%;
      height: 20px;
      width: 8px;
      margin-left: -4px;
      &-inner {
        height: 100%;
        border-radius: 4px;
        background: #C2C2C2;
      }
    }
  }
</style>
  1. 设置滚动条滚动的距离
    根据2中的图片我们可以确定滚动条滚动的距离也是和父元素的高度以及子元素和文档移动的距离有关:文档移动的距离(translateY)/子元素的高度 = 滚动条移动的距离 / 父元素的高度
mounted(){
  parent.addEventListener('wheel', (e) => {
        if (e.deltaY > 20) {
          translateY -= 20 * 3
        } else if (e.deltaY < -20) {
          translateY -= -20 * 3
        } else {
          translateY -= e.deltaY * 3
        }
        if (translateY > 0) {
          translateY = 0
        } else if (translateY < -maxHeight) {
          translateY = -maxHeight
        } else {
          e.preventDefault()
        }
        child.style.transform = `translateY(${translateY}px)`
        this.updateScrollHeight(parentHeight, childHeight, translateY)
      })
      this.updateScrollHeight(parentHeight, childHeight, translateY)
    },
    methods: {
      updateScrollHeight(parentHeight, childHeight, translateY) {
        let bar = this.$refs.scrollBar
        bar.style.height = (parentHeight * parentHeight / childHeight) + 'px'
        this.scrollMoveY = -(translateY * parentHeight / childHeight) + 'px'
      }
    }
scroll支持拖动

drag api的使用

<div id="test" style="height: 100px; width: 100px; border: 1px solid red; position: absolute;top: 0; left: 0; z-index: 1;"
      draggable="true"
    ></div>

mounted() {
  let test = document.querySelector('#test')
  let startPosition
  let endPosition
  test.addEventListener('dragstart', (e) => {
    test.classList.add('hide')
    let {screenX: x, screenY: y} = e
    startPosition = [x,y]
    console.log('startPosition')
    console.log(startPosition)
  })
  test.addEventListener('dragend', (e) => {
    let {screenX: x, screenY: y} = e
    endPosition = [x,y]
    let deltaX = endPosition[0] - startPosition[0]
    console.log(deltaX)
    let deltaY = endPosition[1] - startPosition[1]
    test.style.left = parseInt(test.style.left) + deltaX + 'px'
    test.style.top = parseInt(test.style.top) + deltaY + 'px'
    test.classList.remove('hide')
  })
}

.hide {
  opacity: 0.2;
}

注意事项:
1.使用drag必须在html上设置draggable="true"

  1. 拖动开始的时候给当前元素设置opacity,降低它的透明度,不能设置为0,会直接看不到拖动,隐藏也是一样的
  2. 通过拖动开始和拖动结束的scrollX和scrollY的差值用开始的位置加上差值,就是拖动结束的位置

解决drag元素拖动的时候原来的位置还显示的问题:
通过setTimeout给元素添加一个类,里面设置transform: translateX(-9999px)

test.addEventListener('dragstart', (e) => {
    setTimeout(() => {
      test.classList.add('hide')
    })
    let {screenX: x, screenY: y} = e
    startPosition = [x,y]
  })

.hide {
  transform: translateX(-9999px);
}

新的问题:1. 当我们拖动的时候会发现它会往后回弹一下 2.drag不支持单方向拖动(除非配合插件)

使用mousemove实现滚动条拖动
<div class="lifa-scroll-track" v-show="scrollBarVisible">
      <div class="lifa-scroll-bar" ref="scrollBar" :style="{transform: `translateY(${scrollMoveY})`}"
        @mousedown="onMouseDownScrollBar" @selectstart="onSelectStart"
      >
        <div class="lifa-scroll-bar-inner"></div>
      </div>
    </div>

data () {
  return {
    scrollMoveY: 0,
    scrollBarVisible: false,
    isScrolling: false,
    startPosition: undefined,
    endPosition: undefined,
    translateX: 0,
    translateY: 0,
   }
},
methods: {
  document.addEventListener('mousemove', (e) => {
        this.onMouseMoveScrollBar(e)
      })
      document.addEventListener('mouseup', (e) => {
        this.onMouseUpScrollBar(e)
      })
  onMouseDownScrollBar(e) {
    this.isScrolling = true
    let {screenX, screenY} = e
    this.startPosition = {x: screenX, y: screenY}
},
onMouseMoveScrollBar(e) {
  if (!this.isScrolling) return
  let {screenX, screenY} = e
  let maxHeight = this.parentHeight - this.barHeight
  this.endPosition = {x: screenX, y: screenY}
  let delta = {x: this.endPosition.x - this.startPosition.x, y: this.endPosition.y - this.startPosition.y}
  this.translateY = parseInt(this.translateY) + delta.y
  if (this.translateY < 0) {
    this.translateY = 0
  } else if (this.translateY > maxHeight) {
    this.translateY = maxHeight
  }
  this.startPosition = this.endPosition
  this.$refs.scrollBar.style.transform = `translate(0px,${this.translateY}px)`
},
onMouseUpScrollBar(e) {
  this.isScrolling = false
},
onSelectStart(e) {
  e.preventDefault()
}
}

内容和滚动条同步滚动

export default {
    name: "LiFaUiScroll",
    data () {
      return {
        scrollMoveY: 0,
        scrollBarVisible: false,
        isScrolling: false,
        startPosition: undefined,
        endPosition: undefined,
        scrollBarY: 0,
        barHeight: undefined,
        parentHeight: undefined,
        contentY: 0,
        childHeight: undefined
      }
    },
    mounted() {
      document.addEventListener('mousemove', (e) => {
        this.onMouseMoveScrollBar(e)
      })
      document.addEventListener('mouseup', (e) => {
        this.onMouseUpScrollBar(e)
      })
      let parent = this.$refs.parent
      let child = this.$refs.child
      // 将height命名为childHeight
      let {height: childHeight} = child.getBoundingClientRect()
      let {height: parentHeight} = parent.getBoundingClientRect()
      this.parentHeight = parentHeight
      this.childHeight = childHeight
      let {borderTopWidth, borderBottomWidth, paddingTop, paddingBottom} = window.getComputedStyle(parent)
      borderTopWidth = parseInt(borderTopWidth)
      borderBottomWidth = parseInt(borderBottomWidth)
      paddingTop = parseInt(paddingTop)
      paddingBottom = parseInt(paddingBottom)
      let maxHeight = childHeight - parentHeight + (borderTopWidth+borderBottomWidth+paddingTop+paddingBottom)
      parent.addEventListener('wheel', (e) => {
        if (e.deltaY > 20) {
          this.contentY -= 20 * 3
        } else if (e.deltaY < -20) {
          this.contentY -= -20 * 3
        } else {
          this.contentY -= e.deltaY * 3
        }
        if (this.contentY > 0) {
          this.contentY = 0
        } else if (this.contentY < -maxHeight) {
          this.contentY = -maxHeight
        } else {
          e.preventDefault()
        }
        this.updateScrollHeight(parentHeight, childHeight, this.contentY)
      })
      this.updateScrollHeight(parentHeight, childHeight, this.contentY)
    },
    methods: {
      updateScrollHeight(parentHeight, childHeight, translateY) {
        let bar = this.$refs.scrollBar
        let barHeight = (parentHeight * parentHeight / childHeight)
        this.barHeight = barHeight
        bar.style.height = barHeight + 'px'
        let y = -(translateY * parentHeight / childHeight)
        this.scrollMoveY = y + 'px'
        this.scrollBarY = y
      },
      onMouseEnter() {
        this.scrollBarVisible = true
      },
      onMouseLeave() {
        // this.scrollBarVisible = false
        this.isScrolling = false
      },
      onMouseDownScrollBar(e) {
        this.isScrolling = true
        let {screenX, screenY} = e
        this.startPosition = {x: screenX, y: screenY}
      },
      onMouseMoveScrollBar(e) {
        if (!this.isScrolling) return
        let {screenX, screenY} = e
        let maxHeight = this.parentHeight - this.barHeight
        this.endPosition = {x: screenX, y: screenY}
        let delta = {x: this.endPosition.x - this.startPosition.x, y: this.endPosition.y - this.startPosition.y}
        this.scrollBarY = parseInt(this.scrollBarY) + delta.y
        if (this.scrollBarY < 0) {
          this.scrollBarY = 0
        } else if (this.scrollBarY > maxHeight) {
          this.scrollBarY = maxHeight
        }
        this.contentY = -(this.childHeight * this.scrollBarY / this.parentHeight)
        this.startPosition = this.endPosition
        this.$refs.scrollBar.style.transform = `translate(0px,${this.scrollBarY}px)`
      },
      onMouseUpScrollBar(e) {
        this.isScrolling = false
      },
      onSelectStart(e) {
        e.preventDefault()
      }
    }
  }
代码重构
<template>
  <div class="lifa-scroll-wrapper" ref="parent" @mouseenter="onMouseEnter"
       @mouseleave="onMouseLeave" @wheel="onWheel"
  >
    <div class="lifa-scroll" ref="child" :style="{transform: `translateY(${this.contentY}px)`}">
      <slot></slot>
    </div>
    <div class="lifa-scroll-track" v-show="scrollBarVisible">
      <div class="lifa-scroll-bar" ref="scrollBar" :style="{transform: `translateY(${scrollMoveY})`}"
        @mousedown="onMouseDownScrollBar" @selectstart="onSelectStart"
      >
        <div class="lifa-scroll-bar-inner"></div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: "LiFaUiScroll",
    data () {
      return {
        scrollMoveY: 0,
        scrollBarVisible: false,
        isScrolling: false,
        startPosition: undefined,
        endPosition: undefined,
        scrollBarY: 0,
        barHeight: undefined,
        parentHeight: undefined,
        contentY: 0,
        childHeight: undefined,
        maxHeight: undefined
      }
    },
    mounted() {
      this.listenerDocument()
      this.parentHeight = this.$refs.parent.getBoundingClientRect().height
      this.childHeight = this.$refs.child.getBoundingClientRect().height
      this.maxHeight = this.calculateContentYMax()
      this.updateScrollHeight()
    },
    methods: {
      listenerDocument () {
        document.addEventListener('mousemove', e => this.onMouseMoveScrollBar(e))
        document.addEventListener('mouseup', e => this.onMouseUpScrollBar(e))
      },
      calculateContentYMax () {
        let {borderTopWidth, borderBottomWidth, paddingTop, paddingBottom} = window.getComputedStyle(this.$refs.parent)
        borderTopWidth = parseInt(borderTopWidth)
        borderBottomWidth = parseInt(borderBottomWidth)
        paddingTop = parseInt(paddingTop)
        paddingBottom = parseInt(paddingBottom)
        let maxHeight = this.childHeight - this.parentHeight + (borderTopWidth+borderBottomWidth+paddingTop+paddingBottom)
        return maxHeight
      },
      calculateContentYFromDeltaY (deltaY) {
        if (deltaY > 20) {
          this.contentY -= 20 * 3
        } else if (deltaY < -20) {
          this.contentY -= -20 * 3
        } else {
          this.contentY -= deltaY * 3
        }
      },
      calculateScrollBarYMaxAndMin (e) {
        let maxHeight = this.parentHeight - this.barHeight
        this.endPosition = {x: e.screenX, y: e.screenY}
        let delta = {x: this.endPosition.x - this.startPosition.x, y: this.endPosition.y - this.startPosition.y}
        this.scrollBarY = parseInt(this.scrollBarY) + delta.y
        if (this.scrollBarY < 0) {
          this.scrollBarY = 0
        } else if (this.scrollBarY > maxHeight) {
          this.scrollBarY = maxHeight
        }
      },
      onWheel (e) {
        this.calculateContentYFromDeltaY(e.deltaY)
        if (this.contentY > 0) {
          this.contentY = 0
        } else if (this.contentY < -this.maxHeight) {
          this.contentY = -this.maxHeight
        } else {
          e.preventDefault()
        }
        this.updateScrollHeight()
      },
      updateScrollHeight() {
        this.barHeight = (this.parentHeight * this.parentHeight / this.childHeight)
        this.$refs.scrollBar.style.height = this.barHeight + 'px'
        let y = -(this.contentY * this.parentHeight / this.childHeight)
        this.scrollMoveY = y + 'px'
        this.scrollBarY = y
      },
      onMouseEnter() {
        this.scrollBarVisible = true
      },
      onMouseLeave() {
        // this.scrollBarVisible = false
        this.isScrolling = false
      },
      onMouseDownScrollBar(e) {
        this.isScrolling = true
        let {screenX, screenY} = e
        this.startPosition = {x: screenX, y: screenY}
      },
      onMouseMoveScrollBar(e) {
        if (!this.isScrolling) return
        this.calculateScrollBarYMaxAndMin(e)
        this.contentY = -(this.childHeight * this.scrollBarY / this.parentHeight)
        this.startPosition = this.endPosition
        this.$refs.scrollBar.style.transform = `translate(0px,${this.scrollBarY}px)`
      },
      onMouseUpScrollBar(e) {
        this.isScrolling = false
      },
      onSelectStart(e) {
        e.preventDefault()
      }
    }
  }
</script>

<style scoped lang="scss">
  .lifa-scroll {
    transition: all .05s ease;
    &-wrapper {
      border: 1px solid green;
      overflow: hidden;
      position: relative;
    }
    &-track {
      position: absolute;
      top: 0;
      right: 0;
      width: 14px;
      height: 100%;
      background: #FAFAFA;
      border-left: 1px solid #E8E7E8;
    }
    &-bar {
      position: absolute;
      top: -1px;
      left: 50%;
      height: 20px;
      width: 8px;
      margin-left: -4px;
      &-inner {
        height: 100%;
        border-radius: 4px;
        background: #C2C2C2;
        opacity: .9;
      }
    }
  }
</style>

相关文章

网友评论

      本文标题:滚动组件

      本文链接:https://www.haomeiwen.com/subject/avgjdctx.html