开发过程中,经常会遇到需要处理大量数据的情况,比如列表、历史记录等,通常选择无限加载和分页导航。
传统后端渲染,一般会选择分页导航,它可以轻松跳转,甚至一次跳转几个页面,现在SPA盛行,无限滚动加载是更好的方案,可以给用户更好的体验,尤其是在移动端。
在Awesome Vue中,有如下无限滚动组件
- vue-infinite-loading – An infinite scroll plugin for Vue.js 1.0 & Vue.js 2.0.
- vue-mugen-scroll – Infinite scroll component for Vue.js 2.
- vue-infinite-scroll – An infinite scroll directive for vue.js.
- vue-loop – An infinite content loop component for Vue.js 2.
- vue-scroller – An infinite content loop component for Vue.js 2, including functionalities such as ‘pull-to-refresh’, ‘infinite-loading’, ‘snaping-scroll’.
- vue-infinite-list – An infinite list mixin can recycle dom for Vue.js 2
- vue-infinite-slide-bar – ∞ Infinite slide bar component.
- vue-virtual-infinite-scroll – A vue2 component based on Iscroll, supports big data list with high performance scroll, infinite load and pull refresh.
Intersection Observer API的出现,让开发无限滚动组件变得更加简单方便。
Intersection Observer API
Intersection Observer API提供了一个可订阅的模型,可以观察该模型,以便在元素进入视口时得到通知。
创建一个观察者实例很简单,我们只需要创建一个IntersectionObserver的新实例并调用observe方法,传递一个DOM元素:
const observer = new IntersectionObserver();
const coolElement = document.querySelector("#coolElement");
observer.observe(coolElement);
接下来可以使用回调方式将参数传给InersectionObserver:
const observer = new IntersectionObserver(entries => {
const firstEntry = entries[0];
if (firstEntry.isIntersecting) {
// Handle intersection here...
}
});
const coolDiv = document.querySelector("#coolDiv");
observer.observe(coolDiv);
回调接收entries作为其参数。 这是一个数组,因为当你使用阈值时你可以有几个条目,但事实并非如此,所以只得到第一个元素。
然后可以使用firstEntry.isIntersection属性检查它是否相交。 这是进行异步请求并检索下一个页面的数据。
IntersectionObserver构造函数使用以下表示法接收选项组件作为其第二个参数:
const options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0
};
const observer = new IntersectionObserver(callback, options);
关于options里的参数解释,截自ruanyifeng intersectionobserver_api
==root==:性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点
==rootMargin==:
定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。
这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器
==threshold==:决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。
比如,[0, 0.25, 0.5, 0.75, 1]
就表示当目标元素 0%、25%、50%、75%、100%
可见时,会触发回调函数。
由于需要使用dom元素作为观察者,在Vue中,使用mounted,React中使用componentDidMount
// Observer.vue
export default {
data: () => ({
observer: null
}),
mounted() {
this.observer = new IntersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
// ...
}
});
this.observer.observe(this.$el);
}
};
注意:我们在
[entry] 参数上使用数组解构,使用this.$el作为root以便观察
为了使其可重用,我们需要让父组件(使用Observer组件的组件)处理相交的事件。 为此,可以在它相交时发出一个自定义事件:
export default {
mounted() {
this.observer = new IntersectionObserver(([entry]) => {
if (entry && entry.isIntersecting) {
this.$emit("intersect");
}
});
this.observer.observe(this.$el);
}
// ...
};
<template>
<div class="observer"/>
</template>
组件销毁的时候,记得关闭observer
export default {
destroyed() {
this.observer.disconnect();
}
// ...
};
与==unobserve==不同的是,unobserve关闭当前被观察的元素,而disconnect关闭所有被观察的元素。
<!-- 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>
创建无限滚动组件Vue
假如有如下类似需求
<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: () => ({ page: 1, items: [] }),
async mounted() {
const res = await fetch(
`https://jsonplaceholder.typicode.com/comments?_page=${
this.page
}&_limit=50`
);
this.items = await res.json();
}
};
</script>
引入Observer组件
<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 "./Observer";
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();
},
components: {
Observer
}
};
</script>
将==mounted==钩子里的异步请求移到==methods==里,并加上自增page以及合并items数据
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, …items] 等价于 this.items.concat(items)
到此InfiniteScroll.vue已经完成
<!-- 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 "./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>
值得注意的是,intersection Observer api兼容性并不是太好,经本人测试,chrome上无压力,其余全不兼容,不过可以使用W3C’s Intersection Observer,npm install intersection-observer
,然后在Observer.vue中加入require('intersection-observer');
即可。