iT邦幫忙

2024 iThome 鐵人賽

DAY 20
1

今天,我決心調整學習的方向,證明給阿狗兄看,我並不是只學皮毛。我決定深挖 useInfiniteScroll,這個功能不只是無限滾動那麼簡單,它蘊含著無限延伸的哲理。
https://ithelp.ithome.com.tw/upload/images/20241003/20162115d3rvioJ7Ue.png

實現原理

useInfiniteScroll 的實現原理基於持續監視滾動行為。當滾動達到特定條件(如接近元素邊界)時,便會觸發載入更多內容。

先把原始碼拆成以下幾個點,方便大家好理解:

  1. 滾動監聽
  2. 元素可見性檢測
  3. 載入狀態管理
  4. 非同步的操作處理
  5. 效能最佳化

讓我們逐一詳細解釋這些原理:

1. 滾動監聽

useInfiniteScroll 使用 useScroll 來監聽指定元素的滾動狀態。主要關注元素是否滾動到了指定方向(如底部)的邊緣。

const state = reactive(useScroll(element, {...}))

這允許函式實時追踪滾動位置,並在達到特定條件時觸發載入資料的行為。

2. 元素可見性檢測

使用 useElementVisibility 來檢測被監聽的元素是否在可是範圍內。

const isElementVisible = useElementVisibility(observedElement)

這確保了只有當元素可見時才會觸發載入,避免不必要的載入操作。

3. 載入狀態管理

函式使用 refcomputed 來管理載入狀態:

const promise = ref<any>()
const isLoading = computed(() => !!promise.value)

這允許外部組件輕鬆追踪當前是否正在載入新內容。

4. 非同步的操作處理

當滿足載入條件時,函式會非同步執行 onLoadMore 回調:

promise.value = Promise.all([
  onLoadMore(state),
  new Promise(resolve => setTimeout(resolve, interval)),
])

使用 Promise.all 確保了載入操作和最小間隔都被遵守 (這個技巧在處理動畫載入時很重要。如果動畫只閃現一秒就消失,會給人一種突兀的感覺。)。

5. 效能最佳化

  • 使用 throttle(通過設定 interval)來限制載入頻率。
  • 使用 nextTick 來延遲檢查,避免在同一個渲染週期內多次觸發載入。
nextTick(() => checkAndLoad())

工作流程

  1. 當滾動發生時,useScroll 更新滾動狀態。
  2. 監聽器檢測到狀態變化,觸發 checkAndLoad 函式。
  3. checkAndLoad 檢查是否滿足載入條件(到達邊緣、元素可見、可以載入更多)。
  4. 如果條件滿足且當前沒有進行中的載入,則觸發 onLoadMore 回調。
  5. 載入完成後,重置狀態並在下一個 tick 再次檢查,以處理可能的新內容。

知道大概的原理後,我們來看看原始碼吧

1. 型別定義

type InfiniteScrollElement = HTMLElement | SVGElement | Window | Document | null | undefined

export interface UseInfiniteScrollOptions<T extends InfiniteScrollElement = InfiniteScrollElement> extends UseScrollOptions {
  distance?: number
  direction?: 'top' | 'bottom' | 'left' | 'right'
  interval?: number
  canLoadMore?: (el: T) => boolean
}

這裡定義了幾個重要的型別:

  • InfiniteScrollElement:可以進行無限滾動的元素類型。
  • UseInfiniteScrollOptions:設定無限滾動行為的選項介面。

2. useInfiniteScroll 函式

export function useInfiniteScroll<T extends InfiniteScrollElement>(
  element: MaybeRefOrGetter<T>,
  onLoadMore: (state: UnwrapNestedRefs<ReturnType<typeof useScroll>>) => Awaitable<void>,
  options: UseInfiniteScrollOptions<T> = {},
) {
  // ... 函式主體
}

這是 useInfiniteScroll 的主要函式,它接受三個參數:

  • element:要監聽滾動的元素。
  • onLoadMore:當需要載入更多內容時調用的回調函式。
  • options:控制無限滾動行為的選項。

3. 選項解構與預設值設定

const {
  direction = 'bottom',
  interval = 100,
  canLoadMore = () => true,
} = options

這裡從 options 中解構出需要的選項,並設定預設值:

  • direction:滾動方向,預設為 'bottom'。
  • interval:兩次載入之間的最小間隔,預設為 100ms。
  • canLoadMore:判斷是否可以載入更多的函式,預設總是返回 true。

4. 狀態管理

const state = reactive(useScroll(element, {
  ...options,
  offset: {
    [direction]: options.distance ?? 0,
    ...options.offset,
  },
}))

const promise = ref<any>()
const isLoading = computed(() => !!promise.value)

這裡使用 useScroll 來管理滾動狀態,並創建了 promiseisLoading 來追踪載入狀態。

5. 元素可見性檢查

const observedElement = computed<HTMLElement | SVGElement | null | undefined>(() => {
  return resolveElement(toValue(element))
})
const isElementVisible = useElementVisibility(observedElement)

這段程式碼用於檢查被監聽的元素是否可見。

6. 核心邏輯:檢查並載入

function checkAndLoad() {
  state.measure()
  if (!observedElement.value || !isElementVisible.value || !canLoadMore(observedElement.value as T))
    return

  const { scrollHeight, clientHeight, scrollWidth, clientWidth } = observedElement.value as HTMLElement
  const isNarrower = (direction === 'bottom' || direction === 'top')
    ? scrollHeight <= clientHeight
    : scrollWidth <= clientWidth

  if (state.arrivedState[direction] || isNarrower) {
    if (!promise.value) {
      promise.value = Promise.all([
        onLoadMore(state),
        new Promise(resolve => setTimeout(resolve, interval)),
      ])
        .finally(() => {
          promise.value = null
          nextTick(() => checkAndLoad())
        })
    }
  }
}

這是 useInfiniteScroll 的核心邏輯:

  1. 首先檢查元素是否可見且可以載入更多。
  2. 然後檢查是否已經滾動到指定方向的邊緣,或者內容區域小於可視區域。
  3. 如果滿足條件且當前沒有正在進行的載入,則觸發 onLoadMore 並設置一個最小間隔。
  4. 載入完成後,重置 promise 並在下一個 tick 再次檢查。

7. 監聽與清理

const stop = watch(
  () => [state.arrivedState[direction], isElementVisible.value],
  checkAndLoad,
  { immediate: true },
)

tryOnUnmounted(stop)

這裡設置了一個監聽器,當滾動到達指定方向或元素可見性變化時,觸發 checkAndLoad。同時,在組件卸載時停止監聽。

8. 返回值

return {
  isLoading,
  reset() {
    nextTick(() => checkAndLoad())
  },
}

函式返回一個物件,包含:

  • isLoading:表示當前是否正在載入的計算屬性。
  • reset:一個重置函式,用於強制檢查並可能觸發新的載入。

總結

useInfiniteScroll 結合了 Vue 的響應式系統和 DOM 操作。透過持續監控滾動狀態和元素可見性,在適當的時機載入更多內容。

本身的邏輯上還結合了多個 VueUse 的其他功能(如 useScrolluseElementVisibility),同時處理了非同步操作和效能最佳化。算是一個很好的例子,示範了如何將多個簡單的功能組合成一個好用的工具。

今天就到這啦~如果有任遺漏或是錯誤再麻煩留言讓我知道

話說我這兩天跑去拔智齒要痛死了


上一篇
D-19 useInfiniteScroll 文件說明與範例 - 無限領域展開
下一篇
D-21 用 useInfiniteScroll - 來升級電商前端分頁的瀏覽吧
系列文
不會 VueUse 而被提分手的我30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言