VUE使用IntersectionObserver实现滚动加载

3,399 阅读6分钟

滚动加载都会存在一个问题就是当我不停加载,数据量过大时,由于页面内容存在过多,页面卡顿甚至内存溢出卡死

解决思路:使用IntersectionObserver监听元素,利用监听回调判断元素进入离开视窗的回调做处理,使用padding代替原先被滚动掉的dom

注意的点:

每页的数据必须超出视窗高度,避免监听上滚动和下滚动同时触发,会造成页面一直抖动

遇到需要做场景还原的情况,例如滚动加载的列表页跳页面看详情再回到当前页需要保留原来的位置的,
可以调用getObjectToStorage方法得到滚动加载的基本信息,
再根据自身的业务场景加一些分页信息一起进行缓存,
再次回到页面时可将原来的缓存信息丢给reviewByStorage方法进行场景还原 

IntersectionObserver在Safari浏览器的兼容不是很好,使用补丁去解决这个问题:
在index.html加上判断是否引入补丁即可
<script type="text/javascript">
  if (!('IntersectionObserver' in window)) {
      var script = document.createElement("script");
      script.src = "https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver";
      document.getElementsByTagName('head')[0].appendChild(script);
  }
</script>

踩过的坑

第一个:在低版本的浏览器虽然可以通过polyfill去加载补丁得到IntersectionObserver对象,
但是有些超低的版本是拿不到isIntersecting(判断观察元素从不相交到相交或者相交到不想交)这个属性的,
例如红米安卓6.0版本,发现这个值是undefined不是布尔值

然后这里为什么要加滚动监听呢?
也是这个低版本手机的问题,手机性能很慢,所以我们在滚动的时候由于是一直在渲染数据,当我们滚动过快,
很容易就超过了一两页数据,这个时候由于dom还没生成,但是已经滚动到padding上去了,
那这个时候就会出现空白页,没办法触发观察,这个时候我给他加了个限制,就是判断滚动的大小,
并且限制它当滚动到超出当前内容视窗时,滚动条定位到最前面那条数据或者是最后面那条数据,
确保能进入dom的观察,
所以这个组件只适用于App端不适用于web端,因为web端一旦拉滚动条就会有卡顿感

再说一下做这个滚动监听的问题,在低版本的浏览器下,scollTo()这个方法是不存在的。。。
这他妈也是个大坑,能解决的是使用scollTop直接等于滚动后的高度,
所以我就开始直接使用了scollTop等于去做这个滚动监听还有场景还原,
麻蛋的发现在新的浏览器失效了,只能用scollTo()这个方法去改滚动条位置,
所以应该是这两个应该是不能共存的,所以我又做了判断,看要使用哪个

上组件代码

<template>
  <div class="scollDiv" id="scollDiv">
    <!-- 全量高度的div -->
    <div class="contentDiv" id="contentDiv" :style="contentStyle">
      <!-- 实际渲染也就是观察的div -->
      <div class="showContent" id="showContent">
        <slot v-for="item in itemList" :item="item"></slot>
      </div>
      <div  style="height:30px;line-height:30px;text-align:center;font-size:14px;color:#666">
        <span v-if="loading">{{loadingText}}</span>
        <span v-if="finished">没有了!</span>
      </div>
    </div>
    <!-- 虚拟的dom -->
    <div class="fakeDom" id="fakeDom">
      <slot v-for="item in addArray" :item="item"></slot>
    </div>
  </div>
</template>

<script>
// 确保每页的数据大于视窗高度。否则会出现上下监听同时触发一直页面抖动的问题
// 如果有场景还原时,immediateCheck设置为false,不要一加载完就调用load方法
export default {
  data () {
    return {
      contentDivDom: null,
      topObserver: null,
      bottomObserver: null,
      contentStyle: {
        'padding-top': '0px',
        'padding-bottom': '0px'
      }, // 内容高度去代替滚动掉的dom元素
      addArray: [], // 每次新增的数据
      heightList: [], // 累计高度数组
      currenIndex: 0, // 当前显示的页数下标
      itemList: [], // 当前渲染的数据
      observerBottom: null, // 监听本页最后一个
      observerTop: null, // 监听本页第一个
      pageList: [], // 分页数据,二维数组
      AllList: [] // 全部的数组
    }
  },
  props: {
    immediateCheck: {// 是否加载完成就调用获取内容方法
      type: Boolean,
      default: false
    },
    finished: { // 是否加载完成不再去调用加载方法
      type: Boolean,
      default: false
    },
    loading: { // 是否正在加载,是的话不再去调用加载方法
      type: Boolean,
      default: false
    },
    loadingText: {
      type: String,
      default: '加载中...'
    },
    list: { // 全部的数据
      type: Array,
      default () {
        return []
      }
    }
  },
  mounted () {
    this.contentDivDom = document.getElementById('scollDiv')
    // 监听滚动事件
    this.contentDivDom.addEventListener('scroll', this.scrollFn)

    // 初始化后是否调用load方法
    if (this.immediateCheck) {
      this.$emit('load')
    }
    let Options = {
      root: document.getElementById('scollDiv'),
      rootMargin: '0px',
      threshold: 0
    }
    // 顶部观察
    this.observerTop = new IntersectionObserver(this.upScoll, Options)
    // 底部观察
    this.observerBottom = new IntersectionObserver(this.downScoll, Options)
  },
  watch: {
    list: {
      handler () {
        if (this.list.length === 0) {
          // 重新加载数据
          this.reload()
          return
        }
        if (this.list.length <= this.AllList.length) {
          this.reload()
        }
        // 当传过来的数组发生变化的时候
        this.addArray = this.list.slice(this.AllList.length, this.list.length) // 新增数据
        this.$nextTick(() => {
          // 等虚拟的dom获取到高度后再操作
          let height
          // 新增的高度
          let addHeight = document.getElementById('fakeDom').clientHeight
          if (this.heightList.length) {
            height = this.heightList[this.heightList.length - 1]
          } else {
            height = 0
          }
          this.heightList.push(height + addHeight) // 累计高度数组
          this.AllList = this.list
          this.pageList.push(this.addArray) // 分页数据加上这一页数据
          if (this.pageList.length === 1) {
            // 如果加完,分页数组也才一页,那就渲染这一页数据,并且对数据做监听处理
            this.itemList = this.addArray
            this.addObserve()
            return
          }
          if (this.currenIndex === this.pageList.length - 2) {
            // 判断当前是不是它还停留在最后一页数据
            this.itemList = [...this.pageList[this.currenIndex], ...this.pageList[this.currenIndex + 1]]
            this.contentStyle['padding-top'] = this.getPaddingStr(this.heightList[this.currenIndex - 1])
            this.currenIndex++
            this.addObserve()
          } else {
            // 不是在最后一个了那就给它的下padding加上这个新增的高度就好了
            this.contentStyle['padding-bottom'] = this.getPaddingStr(this.getPaddingNum(this.contentStyle['padding-bottom']) + addHeight)
          }
        })
      },
      deep: true
    }
  },
  methods: {
    scrollFn () {
      // 获取当前scollDiv的上下padding值
      let top = this.getPaddingNum(this.contentStyle['padding-top'])
      let bottom = this.getPaddingNum(this.contentStyle['padding-bottom'])
      let scrollTop = document.getElementById('scollDiv').scrollTop // 滚动高度
      let scrollHeight = document.getElementById('scollDiv').scrollHeight // 实际高度scrollHeight
      let offsetHeight = document.getElementById('scollDiv').offsetHeight // 视窗高度
      // 判断滚动高度如果小于top值,则滚到top位置
      if (top - 1 >= scrollTop) {
        if (document.getElementById('scollDiv').scrollTo) {
          document.getElementById('scollDiv').scrollTo(0, top)
        } else {
          document.getElementById('scollDiv').scrollTop = top
        }
      }
      // 判断滚动高度大于实际高度-bottom值-视窗高度,则滚动到实际高度-bottom值位置-视窗高度
      if (scrollTop >= scrollHeight - bottom - offsetHeight) {
        if (document.getElementById('scollDiv').scrollTo) {
          document.getElementById('scollDiv').scrollTo(0, scrollHeight - bottom - offsetHeight)
        } else {
          document.getElementById('scollDiv').scrollTop = (scrollHeight - bottom - offsetHeight)
        }
      }
    },
    // 获取当前的滚动高度以及数据进行页面缓存
    getObjectToStorage () {
      let obj = {
        AllList: this.AllList,
        pageList: this.pageList,
        itemList: this.itemList,
        contentStyle: this.contentStyle,
        heightList: this.heightList,
        currenIndex: this.currenIndex,
        scrollTop: document.getElementById('scollDiv').scrollTop
      }
      return obj
    },
    // 根据缓存的信息做场景还原
    reviewByStorage (storgaeObj) {
      this.AllList = storgaeObj.AllList
      this.pageList = storgaeObj.pageList
      this.itemList = storgaeObj.itemList
      this.contentStyle = storgaeObj.contentStyle
      this.heightList = storgaeObj.heightList
      this.currenIndex = storgaeObj.currenIndex
      if (storgaeObj.scrollTop) {
        // 滚动高度存在
        this.$nextTick(() => {
          if (document.getElementById('scollDiv').scrollTo) {
            document.getElementById('scollDiv').scrollTo(0, storgaeObj.scrollTop)
          } else {
            document.getElementById('scollDiv').scrollTop = storgaeObj.scrollTop
          }
          // 重新监听
          this.addObserve()
        })
      }
    },
    // 重新加载
    reload () {
      let Options = {
        rott: document.getElementById('scollDiv'),
        rootMargin: '0px',
        threshold: 0
      }
      // 顶部观察
      this.observerTop = new IntersectionObserver(this.upScoll, Options)
      // 底部观察
      this.observerBottom = new IntersectionObserver(this.downScoll, Options)
      this.contentStyle = {
        'padding-top': '0px',
        'padding-bottom': '0px'
      }
      this.addArray = []
      this.heightList = []
      this.currenIndex = 0
      this.itemList = []
      this.pageList = []
      this.AllList = []
    },
    addObserve () {
      if (this.topObserver) {
        this.observerTop.unobserve(this.topObserver)
      }
      if (this.bottomObserver) {
        this.observerBottom.unobserve(this.bottomObserver)
      }
      this.observerTop.disconnect() // 停止观察所有
      this.observerBottom.disconnect() // 停止观察所有
      this.$nextTick(() => {
        let currenPageChildren = document.getElementById('showContent').children
        if (currenPageChildren && currenPageChildren.length) {
          // 观察第一个子节点
          this.topObserver = currenPageChildren[0]
          this.observerTop.observe(this.topObserver)
          // 观察最后一个子节点
          this.bottomObserver = currenPageChildren[currenPageChildren.length - 1]
          this.observerBottom.observe(this.bottomObserver)
        }
      })
    },
    // 向下滚动进入到最后一个
    downScoll (e) {
      if (e[0].intersectionRatio <= 0) return
      if (e[0]) {
        // 从不相交到相交
        // if (e[0].isIntersecting) {
        // 进入最后一行,判断是否有下一页,有的话加载下一页,没有的话就根据状态判断是否要去后台拿数据
        if (this.currenIndex === this.pageList.length - 1) {
          // 最后一页
          if (!this.finished && !this.loading) {
            this.$emit('load')
          }
        } else {
          this.currenIndex++
          this.itemList = [...this.pageList[this.currenIndex - 1], ...this.pageList[this.currenIndex]]
          if (this.currenIndex > 1) {
            this.contentStyle['padding-top'] = this.getPaddingStr(this.heightList[this.currenIndex - 2])
            this.contentStyle['padding-bottom'] = this.getPaddingStr(this.heightList[this.heightList.length - 1] - this.heightList[this.currenIndex])
          }
          this.addObserve()
        }
        // }
      }
    },
    // 向上滚动进入到第一个
    upScoll (e) {
      if (e[0].intersectionRatio <= 0) return
      if (e[0]) {
        // if (e[0].isIntersecting) {
        // 进入第一行,当是0的时候不做处理
        if (this.currenIndex !== 0) {
          this.currenIndex--
          if (this.currenIndex === 0) {
            this.itemList = [...this.pageList[0], ...this.pageList[1]]
            this.contentStyle['padding-top'] = '0px'
            // this.contentStyle['padding-bottom'] = this.heightList[this.heightList.length - 1] - this.heightList[this.currenIndex] + 'px'
          }
          if (this.currenIndex > 1) {
            this.itemList = [...this.pageList[this.currenIndex - 1], ...this.pageList[this.currenIndex]]
            this.contentStyle['padding-top'] = this.getPaddingStr(this.heightList[this.currenIndex - 2])
            this.contentStyle['padding-bottom'] = this.getPaddingStr(this.heightList[this.heightList.length - 1] - this.heightList[this.currenIndex])
          }
          this.addObserve()
        }
        // }
      }
    },
    // 根据xxx px 获取数字
    getPaddingNum (str) {
      let num = str.replace('px', '')
      return Number(num)
    },
    // 根据入参获取 xxx px字符串
    getPaddingStr (num) {
      if (num) {
        return '' + num + 'px'
      } else {
        return '0px'
      }
    }
  },
  destroyed () {
    this.contentDivDom.removeEventListener('scroll', this.scrollFn) // 销毁监听
  }
}
</script>

<style lang="scss">
  .scollDiv{
    width: 100%;
    height: 100%;
    overflow: auto;
    position: relative;
    .contentDiv{
      z-index: 333;
      width: 100%;
      .showContent{
        width: 100%;
      }
    }
    .fakeDom{
      position: absolute;
      z-index: -1;
      width: 100%;
      top:0
    }
  }
</style>


调用:

<scoll ref="scoll" v-if="reloadFlag" style="background-color: #f5f5f5" @load="getList" :list="list" :loading="loading" :finished="finished">
      <template slot-scope="scope">
        // scope.item是当前的遍历对象
      </template>
    </scoll>