Skip to content
On this page

无限滚动

🔗elememt-plus基础用法

效果

使用

vue
<template>
  <div class="outer">
    <ul class="list" 
    :infinite-scroll-delay="300" 
    :infinite-scroll-distance="20" 
    :infinite-scroll-immediate="true"
    :infinite-scroll-disabled="disabled" 
    v-infinite-scroll="load"
    >
      <li v-for="i in count" class="list-item">{{ i }}</li>
    </ul>
    <p v-if="loading" class="tip">加载中...</p>
    <p v-if="noMore" class="tip">没有更多了</p>
  </div>
</template>

<script lang="ts">
  const load = () => {
    loading.value = true
    setTimeout(() => {
      count.value += 2
      loading.value = false
    }, 1000)
  }
</script>

与上一篇无限滚动的区别在于参数来自于元素的属性,检测触底状态,但是思路是一致的

重点

触底状态

滚动元素的内容高度 = 可视区域的高度 + 滚动元素距离顶部的高度

一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0。


所以,el.scrollHeight = el.clientHeight + el.scrollTop 即可以判定已经到达底部

填充父元素

可以设置 immediate属性 ,立即加载数据直到触底为止

原理在于使用 MutationObserver 检测Dom变化

ts
let observe = new MutationObserver(onScroll)
observe.observe(container, {
    childList: true,// 观察目标子节点的变化
    subtree: true // 观察后代节点
})
onScroll()

源码

vue
<template>
  <div class="outer">
    <ul class="list" :infinite-scroll-delay="300" :infinite-scroll-distance="20" :infinite-scroll-immediate="true"
      :infinite-scroll-disabled="disabled" v-infinite-scroll="load">
      <li v-for="i in count" class="list-item">{{ i }}</li>
    </ul>
    <p v-if="loading" class="tip">加载中...</p>
    <p v-if="noMore" class="tip">没有更多了</p>
  </div>
</template>
<script setup lang="ts">
import { throttle } from "../utils/helpers"
import { getOverScrollEle } from "../utils/elements"
import { computed, nextTick, ref } from 'vue';
import type { ObjectDirective } from "vue"

let count = ref(0),
    loading = ref(false)
    
const noMore = computed(() => {
  return count.value >= 20
})
/**
 * 0. 设置默认值
 * 1. 获取元素身上的属性
 * 1. 找到父级元素container 有 overflow 为 auto 或者 scroll
 * 2. 如果有 immediate,那么就立即充满高度,handleScroll
 * 3. 当container 发生滚动的时候,handleScroll 执行节流函数,时间为 delay
 * 4. handleScroll 判断是否触底,如果 el.scrollTop + el.offsetHeight + distance 的高度是否小于 scrollHeight
 * 5. 如果已经 等于,需要把 scroll 解绑
 * 6. 如果没有,需要执行 load 函数
 * 7. 监听 disabled 的变化,如果为 true,解绑
 */

const SCOPE = 'infinite-scroll'


type InfiniteScrollEl = HTMLElement & {
  [SCOPE]: {
    container: HTMLElement
    delay: number // export for test
    cb: Function
    onScroll: () => void
    observer?: MutationObserver,
  }
}

type Option = {
  delay: number
  "immediate": boolean,
  "disabled": boolean,
  "distance": number,
}
type OptionKeys = Array<keyof Option>;

let defaultOption: Option = {
  "delay": 500,
  "immediate": true,
  "disabled": false,
  "distance": 0,
}

function getScrollOptions(el: HTMLElement): Option {
  return ((Object.keys(defaultOption) as OptionKeys).reduce((map, key) => {
    // 去除 infinite-scroll-
    const attrVal = el.getAttribute(`infinite-scroll-${key}`);

    let value;
    if (attrVal) {
      value = attrVal;
    } else {
      value = defaultOption[key]
    }
    // 字符串类型需要转换成布尔类型
    value = value === 'false' ? false : value;
    ; (map[key] as any) = value;

    return map
  }, {} as Option))

};


function handleScroll(el: InfiniteScrollEl, fn: Function) {
  const { observer, container } = el[SCOPE]
  const { disabled, distance } = getScrollOptions(el);

  if (disabled) return;
  //  说明没有触底
  if (container.scrollTop + container.clientHeight + Number(distance) >= container.scrollHeight) {
    fn()
  } else {
    // 已经触底
    // 如果是 immediate 模式,则会有observe
    if (observer) {
      observer.disconnect()
      delete el[SCOPE].observer
    }
  }
}

const vInfiniteScroll: ObjectDirective<InfiniteScrollEl, Function> = {

  async mounted(el, bindings) {
    const {value: cb } = bindings
    await nextTick();

    let { delay, immediate } = getScrollOptions(el);
    let container = getOverScrollEle(el);
    if (!container) return;
    let onScroll = handleScroll.bind(null, el, cb)

    el[SCOPE] = {
      container,
      onScroll,
      delay,
      cb
    }

    if (immediate) {
      let observe = new MutationObserver(onScroll)
      el[SCOPE].observer = observe
      // subtree 可选
      // 当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target
      observe.observe(container, {
        childList: true, // 儿子节点
        // subtree: true // 儿子的儿子
      })
      onScroll()
    }
    container.addEventListener("scroll", throttle(onScroll.bind(null, el), delay))
  },

  unmounted(el) {
    const { onScroll, container } = el[SCOPE]
    if (container) {
      container.removeEventListener("scroll", onScroll)
      el[SCOPE].observer?.disconnect();
      delete el[SCOPE].observer
    }
  }
}

const disabled = computed(() => {
  return loading.value || noMore.value
})

const load = () => {
  loading.value = true
  setTimeout(() => {
    count.value += 2
    loading.value = false
  }, 1000)
}

</script>
<style lang="scss" scoped>
.outer {
  @apply overflow-auto h-[500px] bg-blue-200 w-full rounded-md;

  .list {
    @apply list-none p-0 m-0;

    .list-item {
      @apply h-[80px] mb-2 bg-green-400 text-white font-bold flex cursor-pointer rounded-md justify-center items-center;
    }
  }

  .tip {
    @apply text-gray-700 text-center m-2 font-bold
  }
}
</style>