「實戰」用原生的 Intersection Observer API 實現 Lazy Loading

語言: CN / TW / HK

前一陣子在做一個專案的時候,因為每組資料都要先通過很龐大的計算,才把計算後的結果 Render 到頁面上,但這樣就導致如果單頁查出來的資料超過大概 5 筆,就會需要等待一段有感的時間,才能看到結果出現在畫面上。

後來為了解決這差勁使用者體驗,就使用到的標題上說到的 Lazy Loading 來處理。簡單說就是,雖然要顯示的資料量有 10 筆,但因為一個頁面大概只能呈現 2 到 3 筆,那我就先計算那 2 到 3 筆資料然後顯示就好,剩下的資料等使用者往下滾再繼續顯示,這樣等待時間就不會太久。

然後身為一個前端工程師,再想到這個解法以後,當然就是上 Github 找一個簡單又方便的元件來解決它 ,而最後找到的 vue-scroll-loader 使用起來非常容易,程式碼也少少的,所以就在處理完 issue 後,看它內部是如何實現 Lazy Loading,於是就看到今天主要講的 Intersection Observer API 啦!

Intersection Observer API

那 Intersection Observer API 到底是什麼?為什麼它可以用來實現 Lazy Loading 呢?以 MDN 的說法來說:

Intersection Observer API 提供了一種非同步檢測目標元素與祖先元素或 viewport 相交情況變化的方法。

簡單說的意思就是隻要使用 Intersection Observer API,就能夠監聽目標的元素在畫面上出現或離開的時候,執行你交給它的 callback 方法。下方就來看看使用的方式吧!

使用方法

首先要有簡單的 HTML 和 CSS,主要目標就是把 div 放在往下滾才會出現的地方:

body {
  height: 1000px;
}

.box {
  width: 100px;
  height: 100px;
  background: #000;
  position: absolute;
  top: 500px;
}
<body>
  <div class="box"></div>
</body>

接著我們用 Intersection Observer API 的 observe 方法,把要監聽的 div 當作引數傳給它,並用 callback 讓它可以在 div 出現和離開的時候給個訊息:

const intersectionObserver = new IntersectionObserver(
  () => { console.log('hi'); }
);

intersectionObserver.observe(
  document.querySelector('.box')
);

執行的結果就會像這樣子:

而 Intersection Observer API 在執行 callback 的時候,也會給你一個 Array,Array 是所有正在監聽的元素,我們可以從這些元素裡的 isIntersecting 來判斷當前的元素是出現在畫面中,還是離開畫面了:

const intersectionObserver = new IntersectionObserver(
  (entries) => {
    if (entries[0].isIntersecting) {
      console.log('我進來了!');
    } else {
      console.log('我又出去了!');
    }
  }
);

intersectionObserver.observe(
  document.querySelector('.box')
);

執行結果:

最後就是當你不再需要繼續監聽元素的時候,可以使用 unobserve 來解除監聽,使用時就像監聽用的 observe 一樣,給它不需要再監聽的元素:

intersectionObserver.unobserve(
  document.querySelector('.box')
);

以上就是 Intersection Observer API 的基本用法,當然還有其他比較仔細的設定(可以看 MDN 的介紹),但如果要完成一個簡單的 Lazy Loading,那隻要會上方的幾種使用方式就綽綽有餘了!

Lazy Loading

Intersection Observer API 實現 Lazy Loading 的方法就是在資料列表的最後放個 loading 的小動畫,接著只要去監聽小動畫,當它出現在頁面中的時候,用 Intersection Observer API 的 callback 載入更多資料。

首先一樣先簡單寫個要顯示資料的 <ul> 和要監聽的元素,這裡我就不做小動畫了,直接用 Loading… 文字代替 :

<body>
  <ul class="list"></ul>
  <div class="loading">Loading...</div>
</body>

要注意監聽的元素必須要在載入資料的最下面哦!不然它不會被監聽到“出現在頁面上”了(這個下方會更詳細說明注意事項)。

JavaScript 的部分先貼程式碼,下方再來解釋:

const data = Array.from(Array(200)).map(
  (_value, index) => `第 ${index + 1} 筆資料`
);

const render = () => {
  const list = document.querySelector('.list');

  const LOAD_DATA_COUNT = 50;
  const startLoadIndex = list.childNodes.length;
  const endLoadIndex = startLoadIndex + LOAD_DATA_COUNT;
  for (let i = startLoadIndex; i < endLoadIndex; i++) {
    if (data[i]) {
      const text = document.createTextNode(data[i]);
      const li = document.createElement('li');
      li.appendChild(text);
      list.appendChild(li);
    }
  }
  
  if (endLoadIndex >= data.length) {
    const loading = document.querySelector('.loading');
    loading.style.display = 'none';
    intersectionObserver.unobserve(loading);
  }
};

render();

const intersectionObserver = new IntersectionObserver(
  (entries) => {
    if (entries[0].isIntersecting) {
      setTimeout(render, 1000);
    }
  }
);

intersectionObserver.observe(
  document.querySelector('.loading')
);
  1. 先用迴圈產生 200 筆的假資料
  2. 寫一個 render 的方法,把還沒載入的資料迴圈加去,這裡一次加 50 筆資料
  3. 在 render 內加完資料,去判斷當前加到的 index 有沒有大於資料總數,如果有的話代表所有資料顯示完了,因此隱藏 loading,並移除 Intersection Observer API 對 loading 的監聽
  4. 畢竟一開始畫面上還是要有資料!所以先手動執行第一次 render 方法
  5. 用 Intersection Observer API 監聽 loading,只要一出現在畫面上(代表使用者看完目前的資料,就要在執行 render。這裡為了有真正 render 的感覺,我用 setTimeout 來延遲 1 秒

執行的效果就會像這樣子:

但是還有一點要注意的地方,以上方的例子來說,如果 Intersection Observer API 因為 loading 出現在頁面中執行了 render,但是 render 後的資料量卻不足以把 loading 移到畫面外,那 loading 就會一直停留在畫面中,而不是“出現在畫面中”,這麼一來,Intersection Observer API 也就不會觸發 render 載入更多資料。

最後來看一下支援情況。ntersection Observe API 的支援度算不錯了,但如果產品有要考慮到 IE 的客戶群就沒辦法用了。

最後還是覺得從開源專案裡面以學到很多有趣的東西,也推薦大家可以在使用某些元件時候偷看一下背後的原始碼怎麼處理的。

~完,我是刷碗智,新的一年,我們一起洗刷重新整理!!

作者:神Q超人 譯者:前端小智 來源:medium 原文:https://medium.com/starbugs/%E7%94%A8%E5%8E%9F%E7%94%9F%E7%9A%84-javascript-intersection-observer-api-%E5%AF%AE7%8F%BE-lazy-loading-6bedccd0950