0%

Vue中实现滚动加载

最近在做一个 vue 商城的项目,项目中要求首先加载第一页商城的商品列表,当用户滚动查看商品时,在快到达列表底部的时候提前加载第一页商品列表,即诸如淘宝、天猫、京东商城的滚动懒加载。

分析需求:关键是如何判断在滚动的时候到达列表底部。我们可以在列表底部放一个 div ,判断该 div 出现在可视区域中的时候,即滚动到列表底部。那么如何判断一个元素是否在可视区域中出现呢?

判断元素是否出现在可视区域

首先我们来总结一下几个关键的概念

偏移量

偏移量(offset dimension),元素的可见大小由其高度、宽度决定,包括所有的内边距、滚动条和边框大小,不包含外边距。以下是获取元素偏移量的方法:

  • offsetHeight = content + padding + border + scrollX

    元素在垂直方向上占用的空间大小,以像素计算。包含元素的高度、边框、内边距和元素的水平滚动条(如果存在且渲染的话),不包含:before或:after等伪类元素的高度。

    如果元素被隐藏(例如 元素或者元素的祖先之一的元素的style.display被设置为none),则返回0

    这个属性会被四舍五入为整数,如果需要一个浮点数值,请使用 element.getBoundingClientRect()

  • offsetWidth = content + padding + border + scrollY

    元素在水平方向上占用的空间大小,以像素计算。包含元素的宽度、边框、内边距和元素的竖直滚动条(如果存在且渲染的话),不包含:before或:after等伪类元素的高度。

  • offsetLeft

    元素的左外边框至包含元素的左内边框之间的像素值。

  • offsetTop

    元素的上外边框至包含元素的上内边框之间的像素距离。

如下图所示:

偏移量图示

客户区域大小

客户区域大小有以下两个属性:

  • clientWidth = content + padding

    clientWidth是元素内容区域宽度加上左右内边距宽度

  • clientHeight = content + padding

    clientWidth是元素内容区域高度加上左右内边距高度

可以通过如下方法来确定浏览器视口大小:

1
2
3
4
let viewPort = {
width: document.body.clientWidth || document.documentElement.clientWidth,
height: document.body.clientHeight || document.documentElement.clientHeight
}

客户区大小不包括滚动条、边框、外边距。

滚动大小

  • scrollHeight:在没有滚动条的情况下,元素内容的总高度,即 clientHeight
  • scrollWidth
  • scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。
  • scrollTop

scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。

scrollLeft 和 scrollTop属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位 置。在元素尚未被滚动时,这两个属性的值都等于 0。如果元素被垂直滚动了,那么 scrollTop 的值 会大于 0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么 scrollLeft 的值会 大于 0,且表示元素左侧不可见内容的像素宽度。这两个属性都是可以设置的,因此将元素的 scrollLeft 和 scrollTop 设置为 0,就可以重置元素的滚动位置。

确定元素大小

getBoundingClientRect:一般来 说,right 和 left 的差值与 offsetWidth 的值相等,而 bottom 和 top 的差值与 offsetHeight 相等。

实现判断元素在可视区内

  1. 方法一

    1
    2
    3
    4
    5
    6
    7
    8
    function isInViewPort(el) {
    const viewPortHeight = window.innerHeight || document.body.clientHeight || document.documentElement.clientHeight
    const elOffsetTop = el.offsetTop
    const dScrollTop = document.documentElement.scrollTop
    const top = elOffsetTop - dScrollTop

    return top <= viewPortHeight
    }
  1. 方法二

    1
    2
    3
    4
    5
    6
    function isInViewPort (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
    const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
    console.log('top', top)
    return top <= viewPortHeight
    }

Vue 滚动加载的实现

设计一个 FooterLine 组件,判断该组件是否将要出现在视图中,如将要出现则进行加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div class="footer">
<p>加载中...</p>
</div>
</template>

<script>
export default {
name: "FooterLine",
}
</script>

<style scoped>
.footer {
font-size: 12px;
position: relative;
text-align: center;
margin:12px;
}
.footer p::after, .footer p::before {
content:"";
position: absolute;
width:40%;
height: 1px;
background: #bab6b6;
top: 6px;
right: 0;
}
.footer p::before {
left: 0;
}
</style>

接下来监听页面的滚动事件,当 isInViewPort 函数返回 true 时,表明组件即将进入页面,触发函数加载数据:

1
2
3
4
5
6
7
8
mounted() {
const line = document.querySelector(".footer")
window.onscroll = () => {
if(isInViewPort(line)) {
this.$emit("arrive-bottom")
}
}
},

这样实现之后我们发现,当页面不断滚动的时候,控制台会不断打印出 top 值,并且当 top <= viewPortHeight 时会不断发送 http 请求数据,显然这对页面的性能影响很严重,并且也可能导致不断发送重复请求。经过思考,我们可以使用 vue 提供的 watch 来解决该问题。具体思路是:

  • 首先定义一个数据 IsEmit= false,使用 watch 监听该数据的更改
  • 监听页面的滚动事件,当滚动到接近底部的时候,将 IsEmit 修改为 true
  • watch 监听到 IsEmit 的修改,并且 IsEmit 修改为 true 时,触发函数加载数据。这样即使继续滚动也不会不断发送重复请求

具体代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default {
name: "FooterLine",
data() {
return {
IsEmit: false
}
},
watch: {
IsEmit(newValue) {
if(newValue) {
this.$emit("arrive-bottom")
}
}
},
mounted() {
const line = document.querySelector(".footer")
window.onscroll = () => {
if(this.isInViewPort(line)) {
this.IsEmit = true
} else {
this.IsEmit = false
}
}
},
methods: {
isInViewPort (el) {
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
console.log('top', top)
return top <= viewPortHeight
}
}
}

上述方法利用 vue 的特性结局了不会重复发送 http 请求的问题,但是依然没有解决控制台不断打印 top 值的问题,即在页面滚动过程中,滚动事件执行的过于频繁,但是我们并不希望这么频繁的执行

基于以上问题,我们可以利用防抖函数和节流函数来解决。