基于 IntersectionObserver API 实现无限滚动组件

4,299 阅读4分钟

IntersectionObserver API 是什么?

Intersection Observer API 提供了一种异步观察目标元素与祖先元素或顶级文档 viewport 的交集中的变化的方法。允许你配置一个回调函数,每当目标 (target) 元素与设备视窗或者其他指定元素发生交集的时候执行。

改造 vue-scroll-loader

vue-scroll-loader 是我在使用 element UI 库过程中,由于该库自带的无限滚动组件会有莫名其妙的 Bug,提了 issue 也久久没见修复,于是自(chong)力(fu)更(zao)生(lun)撸(zi)了一个类似的无限滚动组件,足够简单、足够小。

在使用观察者 API 之前,vue-scorll-loader 1.x 版本是使用远古技术通过监听滚动条实现的,稍有常识的人都知道这种方式会有性能损耗 :-D,如今 Intersection Observer API 兼容性越来越好,再加上官方的 polyfill 就可以生产环境使用了,令人惊喜的是亢余的旧代码经过改造之后变为了短短了几行。

一、实现组件主要结构

这里我并没有使用常规的方式创建一个 warpper 包裹要加载的 list ,而是直接使用一个 loading 动画放在 list 下面,loding 即是组件本身,这样组件和 list 解耦,和布局无关,list 要使用无限滚动直接将此组件放下面即可,而不需额外的修改 list 布局

讲到这里,我们就来随手实(fu)现(zhi)一个 loading 动画吧。CSSFX

HTML

使用 slot 提供用户自定义动画的能力

<template lang="html">
  <div class="loader" v-show="!loaderDisable">
    <slot>
      <svg viewBox="25 25 50 50" class="loader__svg" :style="size">
        <circle cx="50" cy="50" r="20" class="loader__circle" :style="color"></circle>
      </svg>
    </slot>
  </div>
</template>

SCSS

<style lang="scss" scoped>
.loader{
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 30px 0;
  &__svg {
    transform-origin: center;
    animation: rotate 2s linear infinite;
  }
  &__circle {
    fill: none;
    stroke-width: 3;
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0;
    stroke-linecap: round;
    animation: dash 1.5s ease-in-out infinite;
  }
}
@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}
@keyframes dash {
  0% {
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0;
  }
  50% {
    stroke-dasharray: 90, 200;
    stroke-dashoffset: -35px;
  }
  100% {
    stroke-dashoffset: -125px;
  }
}
</style>

DEMO

二、实现组件主要功能

以前我们是监听滚动条实现像下面这样:

缺点:同步,且会重复触发,往往需要 debounce 限制频率,需要手动计算相对位置。

window.addEventListener('scroll', e => {
    // 一堆计算代码...
})

而现在使用 Intersection Observer API :

优点:异步,只有处于交叉点才会触发,无需计算元素之间的相对位置。

const observer = new IntersectionObserver(([{ isIntersecting }]) => {
    // 要做什么事情...
}, {
    root: null, // 相对的视口元素,传入 null 则为顶级文档视口
    rootMargin: '0px 0px 0px 0px', // 触发交叉回调时被观察元素相对于视口的偏移量
    threshold: 0 // 一个具体数值或数值数组, 触发交叉回调时被观察元素的可见比例
})
// 传入被观察元素
observer.observe(el)

大家可能注意到了,上面我在回调里面接收了一个 isIntersecting 参数,这是干什么用的呢? 原因是当被观察元素经过和视口的交叉点时,会触发两次回调,一次进入交叉点,一次离开交叉点,进入为 true,离开为 false,所以我们需要这个参数来保证加载回调只触发一次。

下面是结合 Vue 的无限滚动实现。

JAVASCRIPT & VUE

<script>
import 'intersection-observer'
export default {
  name: 'ScrollLoader',
  props: {
    'loader-method': {
      type: Function,
      required: true
    },
    'loader-disable': {
      type: Boolean,
      default: false
    },
    'loader-distance': {
      type: Number,
      default: 0
    },
    'loader-color': {
      type: String,
      default: '#666666'
    },
    'loader-size': {
      type: Number,
      default: 50
    },
    'loader-viewport': {
      type: Element,
      default: null
    }
  },
  computed: {
    size () {
      return {
        width: `${this.loaderSize}px`
      }
    },
    color () {
      return {
        stroke: this.loaderColor
      }
    },
    options () {
      return {
        root: this.loaderViewport,
        rootMargin: `0px 0px ${this.loaderDistance}px 0px`
      }
    },
    observer () {
      return new IntersectionObserver(([{ isIntersecting }]) => {
        isIntersecting && !this.loaderDisable && this.loaderMethod()
      }, this.options)
    }
  },
  mounted () {
    this.observer.observe(this.$el)
  },
  activated () {
    this.observer.observe(this.$el)
  },
  deactivated () {
    this.observer.unobserve(this.$el)
  },
  beforeDestroy () {
    this.observer.unobserve(this.$el)
  }
}
</script>

这里有个技巧,我们 new IntersectionObserver() 会创建一个 observer 观察器,因为当离开当前组件时需要注销观察器,所以这里利用计算属性创建这个观察器的同时保持常驻,相当于简化了在 mounted() 中 new IntersectionObserver() 并把实例存入 data 这一步操作。

DEMO

在线预览:DEMO

项目地址:vue-scroll-loader

结语

第一次在掘金发布文章,算不上什么干货,也不是很高深的技术,但还是希望这篇文章能帮助到像我这样的菜鸡前端,文章如有遗漏或错误的地方,还希望大佬指点,谢谢大家!