iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0
Modern Web

web component - 次世代網頁技術的重要拼圖系列 第 23

web component 的實做- virtualized list

  • 分享至 

  • xImage
  •  

web component是組件,好處之一就是能封裝功能,除了封裝UI之外,封裝Web api也是目標之一。本篇和下一篇文章會以IntersectionObserver這個API來實做virtualized list和infinite-scroller。

目標

這組件是用來把數個節點做成virtualized list,可以加快render的速度和所需資源。二維的組件也可以用相同的思路來做。

技術點

可以參考這篇文章。裡面有提到許多有關virtualized list的技術點

  1. 不在window中的節點不要顯示
  2. 當scroll移動時要能加入被移除的節點

主思路

  1. 使用slot元素的name屬性,可以只顯示特定的子節點
<my-list>
    <!-- 只能出現在<slot></slot>之中 -->
    <div>1</div>
    <!-- 只能出現在<slot name='item'></slot>之中 -->
    <div slot='item'>2</div>
</my-list>
  1. 使用IntersectionObserver這個web api,可以保證節點不在window中時觸發移除事件

這組件的問題

  1. 快速捲動時,節點出現的速度可能比較慢。
  2. 使用slot和shadow DOM只是不顯示給使用者而己,web component的子節點還是存在DOM之中,所以image之類的還是會正常載入。

實做

組件的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))
}

IntersectionObserver相關

  1. 如果組件在顯示區外,就把該組件去除slot屬性,並且移出showNodeWeakSet
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,最小化的做二件事

  1. 計算有那些組件應該顯示
  2. 把那些組件顯示
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);
    }
  }
}

上一篇
web component 的實做- 多選下拉選單組件
下一篇
web component 的實做- infinite-scroller
系列文
web component - 次世代網頁技術的重要拼圖30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言