美文网首页让前端飞Web 前端开发
通过 Vue 实现无限滚动列表(二)—— 代码逻辑与 Demo

通过 Vue 实现无限滚动列表(二)—— 代码逻辑与 Demo

作者: ac68882199a1 | 来源:发表于2018-03-25 19:57 被阅读4533次

上一篇介绍了无限列表实现的简单思路,下面说一下代码的实现逻辑。

主要逻辑通过一个 ScrollManager 类来完成,在这个类中,会根据滚动条高度计算当前需要渲染的 items 以及上下需要支撑起来的高度。该类中主要由如下这些方法:

下面代码注释中,cell 与 item 为同一个东西,都是列表中的一项,因为是先完成的代码后添加的注释,所以请不要在意这些细节:)

class ScrollManager {
    // 构造器方法
    constructor ( {
    list, // 待渲染的列表数据 Array
    scrollViewHeight, // 滚动视图的高度,即滚动区域可见部分的高度
    cellHeight, // 每个 item 的高度,如果设置了该值则认为是固定高度列表
    cellCacheNumber, // 上下两方缓冲的item数量
    firstRenderNumber // 动态高度时单屏初次渲染的列表数量
  } ) { ... }

    // 初始化滚动列表
  // 计算首屏需要渲染的items和缓冲items
  initScroll () { ... }

    // 滚动时更新数据
  // 根据滚动条高度计算已经划出屏幕并且不再需要渲染的items
  // 更新需要渲染的items和缓冲items
  // 并更新列表上方和下方需要支撑起的高度
  updateScroll (scrollTop) { ... }

    // 内部调用的调整items相关数据的方法
  // 包括已经不需要渲染的items和需要渲染的items
  _adjustCells () { ... }

    // 动态高度时根据已缓存的cell高度计算平均高度,方法接受当前渲染的cells的高度数组
  // 对已经渲染过的cell高度进行缓存,保证上方的支撑高度计算准确
  // 对未渲染过的cell高度进行预估,保证下方的支撑高度尽量靠近实际高度
  // 调整整个滑动列表的总高度
  updateCellHeight (cellsHeightInfo) { ... }

  // 获取待渲染的items及相关数据
  getRenderInfo () { ... }
}

当然,上面这个类完全可以脱离 Vue 来使用,那这个类是怎么使用的呢?

// 1. 实例化 ScrollManager 类
const manager = new ScrollManager({ ... })

// 2. 实例化完成后,通过 getRenderInfo 获取首次渲染的数据
let renderList = manager.getRenderInfo()

// 3. 需要注意的是,当列表重新渲染后可能会引发滚动条位置的改变,所以需要在页面完成渲染后重新将滚动条定位到准确的位置
// $scrollElement 为滚动列表容器
// lastScrollTop 为上一次滚动后的滚动条高度,初始值为 0
// 该值需要在每次触发滚动事件时进行更新
$scrollElement.scrollTop = lastScrollTop

// 4. 对于高度不定的列表来说,需要在渲染完成后调用更新cell高度的方法
manager.updateCellHeight([cellHeight1, cellHeight2, ...])
// 可以通过下面的方式获取到cell的高度值
// $cell 为单个cell节点
let height = $cell.getBoundingClientRect().height

// 5. 最重要的一点是,需要监听滚动列表容器的滚动时间,监听到滚动后触发 manager 的更新列表方法并更新 lastScrollTop
// 然后重复执行 2 3 4 步
$scrollElement.onScroll = () => {
  lastScrollTop = this.$refs.$scroll.scrollTop
  manager.updateScroll(lastScrollTop)
    // TODO 2,3,4
} 

以上就是整个 demo 的代码逻辑了,并不负责。当然,暂时还有一些功能并没有去实现,比如上一篇说到的一些问题。这篇再提出两个问题:

当快速滚动列表时,或突然将高度定位到某一点时,对于不定高度的列表来说,由于上面的列表项还未来得及渲染和计算高度,此时会出现比较大的bug,上下支撑和整体高度计算都会出现比较大甚至很大的误差。
如果只是用在移动端,滚动速度并不会很快,所以在移动端使用时并不会出现明显的bug.

当列表中的数据更新时(如从原来的 20 项变为 30 项),此时需要对所有的渲染数据进行更新,包括上下撑起的高度、总高度以及不定高度列表的 cellHeight,同时还要保证滚动条的位置不变,即使新增了数据,用户看到的依然是未新增之前的内容。
这一点比较容易实现,但是由于时间原因并没有去完成。感兴趣的小伙伴可以自己完成以下。

次人君的Vue组件库

正文内容到此结束,下面附上整个demo的源码,不想去 github 看的小伙伴可以直接看这里

Demo文件 Scroll.vue

<template>
<InfiniteScroll :list="cells" :scrollViewHeight="736">
  <div slot="cell" slot-scope="props"
  :style="props.cell.style">{{props.cell.text}}</div>
</InfiniteScroll>
</template>

<script>
import InfiniteScroll from '@/src/infiniteScroll/InfiniteScroll'

export default {
  name: 'Scroll',
  components: { InfiniteScroll },
  computed: {
    cells () {
      return new Array(1000).fill(1).map((item, index) => {
        return {
          style: {
            height: Math.floor(Math.random() * 100 + 100) + 'px',
            // height: '100px',
            color: '#ffffff',
            fontSize: '30px',
            background: this.getRandomColor()
          },
          text: '#' + (index + 1)
        }
      })
    }
  },
  methods: {
    getRandomColor () {
      const colors = new Array(3).fill(1).map(item => Math.floor(Math.random() * 255))
      return `rgb(${colors.join(',')})`
    }
  }
}
</script>

无限滚动组件文件 InfiniteScroll.vue

<template>
<div class="t-scroll"
     ref="$scroll"
     :style="{ height: this.scrollViewHeight + 'px' }"
     @scroll.passive="onScroll">
  <div class="t-scroll-padding-top" :style="{height: scrollData.paddingTop + 'px'}"></div>
    
    <div ref="$cell" v-for="item in scrollData.displayCells">
      <slot name="cell" :cell="item"></slot>
    </div>

  <div class="t-scroll-padding-bottom" :style="{height: scrollData.paddingBottom + 'px'}"></div>
</div>
</template>

<script>
import ScrollManager from './ScrollManager'

let manager
let lastScrollTop = 0
let heightFixed = true

export default {
  name: 'InfiniteScroll',
  props: {
    scrollViewHeight: {
      type: Number,
      required: true
    },

    list: {
      type: Array,
      required: true
    },
    // cell缓存数量 即不在可视区域内的预加载数量
    cellCacheNumber: {
      type: Number,
      default: 3
    },
    // cell高度值 如果为0或不传则为动态高度 不为0则为固定高度
    cellHeight: {
      type: Number,
      default: 0
    },

  },
  data () {
    return {
      scrollData: {
        scrollHeight: 0,
        paddingTop: 0,
        paddingBottom: 0,

        displayCells: []
      }
    }
  },
  
  methods: {
    initScrollManager () {
      manager = new ScrollManager({
        list: this.list,
        scrollViewHeight: this.scrollViewHeight,
        cellHeight: this.cellHeight,
        cellCacheNumber: this.cellCacheNumber,
        firstRenderNumber: 10
      })
    },

    updateScrollRender () {
      this.scrollData = manager.getRenderInfo()
      this.$forceUpdate()
      // 更新完成后矫正滚动条位置
      this.$nextTick(() => {
        this.$refs.$scroll.scrollTop = lastScrollTop
        if (!heightFixed) manager.updateCellHeight(
          this.$refs.$cell.map(item => item.getBoundingClientRect().height)
        )
      })
    },


    onScroll () {
      lastScrollTop = this.$refs.$scroll.scrollTop
      manager.updateScroll(lastScrollTop)
      this.updateScrollRender()
    }


  },
  watch: {
    list () {
      manager.updateList(this.list)
    }
  },
  mounted () {
    if (!this.cellHeight) heightFixed = false
    this.initScrollManager()
    this.updateScrollRender()
  }
}
</script>

<style scoped>
.t-scroll  {
  position: relative;
  background: #eeeeee;
  overflow: scroll;
}
.t-scroll-cell {
  color: #ffffff;
  font-size: 30px;
  font-weight: bolder;
}
</style>

无限滚动类文件 ScrollManager.js

export default class ScrollManager {
  // 构造器方法
  constructor ( {
    list, // 待渲染的列表数据 Array
    scrollViewHeight, // 滚动视图的高度,即滚动区域可见部分的高度
    cellHeight, // 每个 item 的高度,如果设置了该值则认为是固定高度列表
    cellCacheNumber, // 上下两方缓冲的item数量
    firstRenderNumber // 动态高度时单屏初次渲染的列表数量
  } ) {
    // 滚动可视区域与滚动列表高度
    this.scrollViewHeight = this.scrollHeight = scrollViewHeight
    // cell平均高度 等于0则为动态高度
    this.cellHeight = cellHeight
    this.heightFixed = cellHeight ? true : false
    // 预加载的cell数量
    this.cellCacheNumber = cellCacheNumber || 3
    // 单屏渲染数量
    this.renderNumber = firstRenderNumber || 10

    // 滚动区域上下撑开的高度
    this.paddingTop = this.paddingBottom = 0
    // cell的高度数据缓存,只在不固定高度时有效
    this.heightCache = new Array(list ? list.length : 0).fill(this.cellHeight)
    // 渲染列表
    this.list = list
    // 待渲染列表
    this.displayCells = []
    // 当前待渲染列表的第一个元素为在全部列表中的位置
    this.passedCells = 0
    // 当前渲染的cells的总高度
    this.currentCellsTotalHeight = 0

    this.initScroll()
  }

  // 初始化滚动列表
  // 计算首屏需要渲染的items和缓冲items
  initScroll () {
    if (this.heightFixed) { // cell高度固定时,校正滑动区域总高度,计算单屏渲染的cell数量及底部支撑高度
      this.scrollHeight = this.list.length * this.cellHeight
      this.renderNumber = Math.ceil(this.scrollViewHeight / this.cellHeight)
      this.displayCells = this.list.slice(0, this.renderNumber + this.cellCacheNumber * 2)
      this.paddingBottom = this.scrollHeight - this.displayCells.length * this.cellHeight
    } else { // cell高度不固定时,渲染初次加载的单屏cell数量
      this.displayCells = this.list.slice(0, this.renderNumber + this.cellCacheNumber * 2)
    }
  }

  // 滚动时更新数据
  // 根据滚动条高度计算已经划出屏幕并且不再需要渲染的items
  // 更新需要渲染的items和缓冲items
  // 并更新列表上方和下方需要支撑起的高度
  updateScroll (scrollTop) {
    if (this.heightFixed) {
      this.passedCells = Math.floor(scrollTop / this.cellHeight)

      this._adjustCells()
      
      this.currentCellsTotalHeight = this.displayCells.length * this.cellHeight
      this.paddingTop = this.passedCells * this.cellHeight 
    } else {
      let passedCellsHeight = 0
      for (let i = 0; i < this.heightCache.length; i++) {
        
        if (scrollTop >= passedCellsHeight) this.passedCells = i
        else break
        passedCellsHeight += this.heightCache[i] ? this.heightCache[i] : this.cellHeight
      }
      
      this._adjustCells()

      this.paddingTop = this.heightCache.reduce((sum, height, index) => {
        if (index < this.passedCells) return sum + height
        return sum
      }, 0)
    }
    this.paddingBottom = this.scrollHeight - this.paddingTop - this.currentCellsTotalHeight
    if (this.paddingBottom < 0) this.paddingBottom = 0
  }

  // 内部调用的调整items相关数据的方法
  // 包括已经不需要渲染的items和需要渲染的items
  _adjustCells () {
    this.passedCells = this.passedCells > this.cellCacheNumber ? this.passedCells - this.cellCacheNumber : 0
    this.displayCells = this.list.slice(this.passedCells, this.renderNumber + this.cellCacheNumber * 2 + this.passedCells)
  }

  // 动态高度时根据已缓存的cell高度计算平均高度,方法接受当前渲染的cells的高度数组
  // 对已经渲染过的cell高度进行缓存,保证上方的支撑高度计算准确
  // 对未渲染过的cell高度进行预估,保证下方的支撑高度尽量靠近实际高度
  // 调整整个滑动列表的总高度
  updateCellHeight (cellsHeightInfo) {
    if (this.heightFixed) return

    // 更新平均cell高度
    this.currentCellsTotalHeight = cellsHeightInfo.reduce((sum, height) => sum + height, 0)
    this.cellHeight = Math.round(this.currentCellsTotalHeight / cellsHeightInfo.length)
    this.renderNumber = Math.ceil(this.scrollViewHeight / this.cellHeight)
    // 保存已知cell的高度信息
    this.heightCache.splice(this.passedCells, cellsHeightInfo.length, ...cellsHeightInfo)
    // 预估滑动区域总高度
    this.scrollHeight = this.heightCache.reduce((sum, height) => {
      if (height) return sum + height
      return sum + this.cellHeight
    }, 0)
  }

  // 获取待渲染的items及相关数据
  getRenderInfo () {
    return {
      scrollHeight: this.scrollHeight,
      paddingTop: this.paddingTop,
      paddingBottom: this.paddingBottom,
      displayCells: this.displayCells
    }
  }
}
扫码关注微信公众号【前端程序员的斜杠青年进化录】 微信扫码,给我赞赏一下~

相关文章

网友评论

    本文标题:通过 Vue 实现无限滚动列表(二)—— 代码逻辑与 Demo

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