使用Intersection Observer API构建无限滚动组件
特别声明:本文根据@Alex Jover Morales的《Build an Infinite Scroll component using Intersection Observer API》一文所整理。
在开发过程中,经常会遇到要处理大量数据的情况,比如列表、搜索记录等,因此你需要一种方法让用户以块状显示这些数据,以便保持应用程序性能和数据的有序性。
你可能会使用分页组件来处理,它可以轻松跳转,甚至一次跳转几个页面。
当然,元素滚动是分页组件的另一种替代方案,它可以提供更好的用户体验,特别是在移动端和可触摸设备上。当用记在页面上滚动时,它提供了一个透明的分页,给人一种没有结尾列表的感觉。
自从Intersection Observer API出现之后,构建无限滚动组件变得更简单。让我们看看如何通过这个API来构建无限滚动组件。
Intersection Observer API
Intersection Observer API提供了一个可订阅的模型,可以观察该模型,以便在元素进入视窗时得到通知。
创建一个观察者实例很简单,我们只需要创建一个IntersectionObserve
的新实例并调用observe
方法,传递一个DOM元素:
const observer = new IntersectionObserver()
const coolElement = document.querySelector('#coolElement')
observer.observe(coolElement)
但是,当观察者进入coolElement
视窗时,我们如何得到通知呢?IntersectionObserver
构造函数接受一个回调作为参数,我们可以这样使用它:
const observer = new IntersectionObserver(entries => {
const firstEntry = entries[0]
if (firstEntry.isIntersecting) {
// Handle intersection here...
}
})
const coolDiv = document.querySelector('#coolDiv')
observer.observe(coolDiv)
如你所见,回调将接收entries
作为其参数。它是一个数组,因为当你使用阈值时,你可以有几个entries
,但事实并非如此,所以我们只会得到第一个元素。然后我们可以使用firstEntry.isIntersection
属性来检查它是否相交。这是进行异步请求并检索下一个页面数据的好方法。
IntersectionObserver
构造函数使用下面的方法接受选项对象(options
)为其第二个参数:
const options = {
root: document.querySelector('#scrollArea')
rootMargin: '0px'
threshold: 1.0
}
const observer = new IntersectionObserver(callback, options)
rootMargin
对于我们的示例非常有用,因为它提供了一种定义margin
的方法,观察者可以使用它来查找交集。默认情况下,它是0
,表示观察者一进入视窗就会触发交叉事件(Intersect Event)。但是设置一个400px
的rootMargin
意味着交叉回调将在元素进入视窗之前400px
位置处触发。
因为root
和threshold
对于这种呢况没有什么意义(因此超出了范围),有关于这方面可以查阅文档进行了解。
知道如何使用交点观察器,我们可以在列表末尾放置一个Observable
组件,以便在用户到达列表底部时添加更多数据。
Observer组件
前面的例子很酷,对吧?但是对于我们来说有一个Vue组件是很方便的,所以我们可以在我们的Vue应用程序中使用它。
我们可以使用一个mounted
钩子来创建我们需要保存在组件状态变量中的观察者。使用mounted
钩子而不是created
钩子很重要,因为我们需要一个DOM元素来观察,而在created
钩子中我们没有它:
// Observer.vue
export default {
data: () => ({
observer: null
}),
mounted() {
this.observer = new IntersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
// ...
}
})
this.observer.observe(this.$el)
}
}
注意:我们在
[entry]
参数上使用数组解构。这是一种速记方式,相当于获取entries
数组并将第一个元素作为entries[0]
访问。
正如你所见,我们使用this.$el
作为root
元素以便观察DOM元素。
为了使其可重用,我们需要让父组件(使用Observer
组件的组件)处理交叉的事件。为此,我们在它相交时发出一个自定义事件intersect
:
export default {
mounted() {
this.observer = new intersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
this.$emit('intersect')
}
})
this.observer.observe(this.$el)
}
// ...
}
根据组件的模板,我们只需要任何元素,所以我们可以使用一个没有任何大小的<div>
:
<template>
<div class="observer"/>
</template>
最后,在组件被销毁时清理观察者很重要,否则,我们将在应用程序中会造成内存泄漏,因为事件监听器不会被清除。我们可以在destroyed
钩子中来调用observer
的disconnect
方法:
export default {
destroyed() {
this.observer.disconnect()
}
// ...
}
你会发现还有unobserve
方法。主要区别是:
unobserve
:停止观察一个元素disconnect
:停止观察所有元素
在我们的示例中,因为我们只有一个元素,所以它们都可以工作。
我们还可以添加一个options
属性,以便在需要使用rootMargin
的情奖品下将IntersectionObserver
选项传递给它。
将所有内容放在Observer.vue
组件中:
<!-- Observer.vue -->
<template>
<div class="observer" />
</template>
<script>
export default {
props: ['options'],
data: () => ({
observer: null
}),
mounted() {
const options = this.options || {}
this.observer = new IntersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
this.$emit('intersect')
}
}, options)
this.observer.observe(this.$el)
},
destroyed() {
this.observer.disconnect()
}
}
</script>
构建元限滚动
假设你有一个列表组件,类似以下内容:
<template>
<div>
<ul>
<li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script>
export default {
data: () => ({
items: []
}),
async mounted() {
const res = await fetch('https://jsonplaceholder.typicode.com/comment')
this.items = await res.json()
}
}
</script>
请注意,代码中使用了
async
和await
语法,使异步代码看起来很漂亮。有关于这方面的信息可以阅读这篇文章。
这个组件使用v-for
将items
渲染到列表中。在mounted
钩子中,它使用Fetch
API从jsonplaceholder.typicode.com
中获取一些模拟数据,用于填充items
变量。
添加分页
它可以正常工作,但没有分页功能。为此,jsonplaceholder.typicode.com
的端点允许我们使用_page
和_limit
来控制返回的数据。此外,我们需要一个page
变量来跟踪,它从1
开始。
我来改变上面的代码,以便进行分页:
export default {
data: () => ({
page: 1,
items: []
}),
async mounted() {
const res = await fetch(
`https://jsonplaceholder.typicode.com/comments?_page=${this.page}&_limit=50`
)
this.items = await res.json()
}
}
现在我们有分页功能了,每个页限制50
个元素。
添加Observer组件
我们仍然需要创建无限滚动组件,接下来把Observer
组件引入进来。我们将在列表底部使用它,当它到达视窗时,它将获取下一页并增加该页。
首先,导入Observer
组件,并将其添加到InfiniteScroll
组件中:
<template>
<div>
<ul>
<li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<Observer @intersect="intersected" />
</div>
</template>
<script>
import Observer from './components/Observer'
export default {
// ...
components: {
Observer
}
}
</script>
最后,我们可以在mounted
钩子上的代码移动intersected
方法中。在Observer
组件的intersect
定制事件。
export default {
data: () => ({
page: 1,
items: []
}),
methods: {
async intersected() {
const res = await fetch(`https://jsonplaceholder.typicode.com/comments?_page=${this.page}&_limit=50`)
}
this.page++
const items = await res.json()
this.items = [...this.items, ...items]
}
}
请记信,我们必须增加页面。此外,现在我们必须将项添加到现有的this.items
数组。我们通过在this.items=[...this.items, ...items]
来实现。这基本上和老办法this.items = this.items.concat(items)
是一样的。
无限滚动组件,全部代码看起来像下面这样:
<!-- InfiniteScroll.vue -->
<template>
<div>
<ul>
<li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<Observer @intersect="intersected" />
</div>
</template>
<script>
import Observer from './components/Observer'
export default {
data: () => ({
page: 1,
items: []
}),
methods: {
async intersected() {
const res = await fetch(`https://jsonplaceholder.typicode.com/comments?_page=${this.page}&_limit=50`)
this.page++
const items = await res.json()
this.items = [...this.items, ...items]
}
},
components: {
Observer
}
}
</script>
总结
无限滚动组件是数据分页的一种很好的展示方式,特别是在移动设备和可触摸设备上。通过添加IntersectionObserver
API,它变得更加容易。在本文中,我们已经完成了自己构建一个无限滚动组件所有步骤。
请记住,如果你需要支持旧的浏览器,你可能需要W3C的IntersectionObserver
和Github上Fetch Polyfill。
最终Demo效果如下: