web component是組件,好處之一就是能封裝功能,除了封裝UI之外,封裝Web api也是目標之一。本篇和下一篇文章會以IntersectionObserver這個API來實做virtualized list和infinite-scroller。
這組件是用來把數個節點做成virtualized list,可以加快render的速度和所需資源。二維的組件也可以用相同的思路來做。
可以參考這篇文章。裡面有提到許多有關virtualized list的技術點
<my-list>
<!-- 只能出現在<slot></slot>之中 -->
<div>1</div>
<!-- 只能出現在<slot name='item'></slot>之中 -->
<div slot='item'>2</div>
</my-list>
組件的API會參考react-window,雖然這個庫現在己經沒在更新了,但這個庫是我第一個參考的庫
<body>
<my-list height="500" width="120" itemSize="100 " itemCount="15">
<my-list-item><img src="https://fakeimg.pl/98x98/ff0000/?text=1" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/00ff00/?text=2" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/0000ff/?text=3" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/ff0000/?text=4" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/00ff00/?text=5" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/0000ff/?text=6" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/ff0000/?text=7" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/00ff00/?text=8" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/0000ff/?text=9" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/ff0000/?text=10" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/00ff00/?text=11" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/0000ff/?text=12" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/ff0000/?text=13" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/00ff00/?text=14" loading="lazy"></my-list-item>
<my-list-item><img src="https://fakeimg.pl/98x98/0000ff/?text=15" loading="lazy"></my-list-item>
</my-list>
<script type="module">
import { myList, myListItem } from "./list.js";
customElements.define("my-list", myList);
customElements.define("my-list-item", myListItem);
</script>
</body>
showNodeWeakSet:存入會顯示的子組件
height = 500
width = 150
itemSize = 100
itemCount = 15
indexNodeMap = new Map();
showNodeWeakSet = new WeakSet();
observer = null
hasShowLock = false;
topCount = 1
bottomCount = 1
這裡就做四件事,設定IntersectionObserver、把子組件設定inline-style再存入Map、顯示子組件和綁上事件
connectedCallback() {
this.setObserver()
this.setScrollData();
const firstIndex = 0;
const lastIndex = Math.floor(this.height / this.itemSize)
this.showItem(firstIndex, lastIndex)
this.containerNode.addEventListener('scroll', this.scrollEvent.bind(this))
}
disconnectedCallback() {
this.indexNodeMap.clear()
this.observer.disconnect()
this.containerNode.removeEventListener('scroll', this.scrollEvent.bind(this))
}
observeCallback = (entries, observer) => {
entries.forEach(({intersectionRatio, target}) => {
if (intersectionRatio <= 0) {
target.removeAttribute('slot');
observer.unobserve(target);
this.showNodeWeakSet.delete(target);
}
})
}
setObserver() {
const observeOptions = {
root: this.containerNode,
rootMargin: `${ this.topCount * this.itemSize}px 0px ${ 1 * this.itemSize}px 0px`,
threshold: 0
}
this.observer = new IntersectionObserver(this.observeCallback, observeOptions)
}
把每個字組件設定inline-style,並且加入indexNodeMap
setScrollData() {
let index = 0
let ScrollTop = 0
for ( const item of this.children) {
item.style = `
position: absolute;
top: ${ScrollTop}px;
left: 0;
`
ScrollTop += this.itemSize;
this.indexNodeMap.set(index, item);
index += 1;
}
}
利用hasLock這個互斥鎖和內建的requestAnimationFrame,最小化的做二件事
scrollEvent() {
requestAnimationFrame(() => {
if (!this.hasLock) {
this.hasLock = true;
const [firstIndex, lastIndex] = this.calcIndex()
this.showItem(firstIndex, lastIndex)
this.hasLock = false;
}
})
}
calcIndex(){
const top = this.containerNode.scrollTop;
const range = top + this.height + ( this.topCount + this.bottomCount ) * this.itemSize
const first = Math.floor(top / this.itemSize);
const last = Math.ceil(range / this.itemSize);
return [first, last]
}
根據輸入的索引值,如果組件不存在於showNodeWeakSet,就設定slot屬性,並且把該組件加入到showNodeWeakSet中。
showItem(firstIndex, lastIndex) {
for (let index = firstIndex; index <= lastIndex; index++) {
if (index < 0 || index >= this.itemCount) {
break;
}
const item = this.indexNodeMap.get(index);
if (!this.showNodeWeakSet.has(item)) {
item.setAttribute('slot', 'item');
this.observer.observe(item);
this.showNodeWeakSet.add(item);
}
}
}