本文章翻譯自:https://vueschool.io/articles/vuejs-tutorials/build-an-infinite-scroll-component-using-intersection-observer-api/
開發過程中,經常會遇到需要處理大量資料的情況,比如列表、歷史記錄等,通常選擇無限載入和分頁導航。
傳統後端渲染,一般會選擇分頁導航,它可以輕鬆跳轉,甚至一次跳轉幾個頁面,現在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(https://github.com/w3c/IntersectionObserver/tree/master/polyfill),npm install intersection-observer
,然後在Observer.vue中加入require('intersection-observer');
即可。
Demo在此:https://codesandbox.io/s/kxm8wlnn85