iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 21

[Day 21] useScroll - arrivedState

  • 分享至 

  • xImage
  •  

官方 Demo:https://vueuse.org/core/useScroll/#usescroll

昨天看了 Demo 中的 X positionY position 是怎麼來的,接下來繼續看 Demo 中 Top ArrivedRight ArrivedBottom ArrivedLeft Arrived 這四個 Boolean 的計算方式。

arrivedState

Top ArrivedRight ArrivedBottom ArrivedLeft Arrived 這四個 Boolean 是來自於 arrivedState 這個 reactive 物件。

來看一下相關程式碼:

const ARRIVED_STATE_THRESHOLD_PIXELS = 1

export function useScroll(element, options = {}) {
  const {
    offset = {
      left: 0,
      right: 0,
      top: 0,
      bottom: 0,
    },
  } = options

  const arrivedState = reactive({
    left: true,
    right: false,
    top: true,
    bottom: false,
  })
  
  // ...略

  const setArrivedState = (target) => {
    if (!window)
      return

    const el = (
      (target)?.document?.documentElement
      || (target)?.documentElement
      || unrefElement(target)
    )

    const { display, flexDirection } = getComputedStyle(el)

    const scrollLeft = el.scrollLeft

    const left = Math.abs(scrollLeft) <= (offset.left || 0)
    const right = Math.abs(scrollLeft)
      + el.clientWidth >= el.scrollWidth
      - (offset.right || 0)
      - ARRIVED_STATE_THRESHOLD_PIXELS

    if (display === 'flex' && flexDirection === 'row-reverse') {
      arrivedState.left = right
      arrivedState.right = left
    }
    else {
      arrivedState.left = left
      arrivedState.right = right
    }

    let scrollTop = el.scrollTop

    // patch for mobile compatible
    if (target === window.document && !scrollTop)
      scrollTop = window.document.body.scrollTop
      
    const top = Math.abs(scrollTop) <= (offset.top || 0)
    const bottom = Math.abs(scrollTop)
      + el.clientHeight >= el.scrollHeight
      - (offset.bottom || 0)
      - ARRIVED_STATE_THRESHOLD_PIXELS

    if (display === 'flex' && flexDirection === 'column-reverse') {
      arrivedState.top = bottom
      arrivedState.bottom = top
    }
    else {
      arrivedState.top = top
      arrivedState.bottom = bottom
    }
  }

  const onScrollHandler = (e) => {
    if (!window)
      return

    const eventTarget = (
      (e.target).documentElement ?? e.target
    )

    setArrivedState(eventTarget)
  }

  useEventListener(
    element,
    'scroll',
    onScrollHandler,
    eventListenerOptions,
  )

  return {
    x,
    y,
    arrivedState,
  }
}

水平、垂直捲軸的計算邏輯是一樣的,先聚焦在水平捲軸的計算:

const left = Math.abs(scrollLeft) <= (offset.left || 0)
const right = Math.abs(scrollLeft)
  + el.clientWidth >= el.scrollWidth
  - (offset.right || 0)
  - ARRIVED_STATE_THRESHOLD_PIXELS

left 的計算滿單純的,scrollLeft 是 0 的時候,捲軸一定在最左邊。

那 Math.abs 是怎麼回事?這邊用絕對值是因為 iOS safari 中預設的捲軸行為,可以嘗試用 iphone safari 開啟官方 Demo,一直把捲軸往左捲動,會發現 Position X 變為負值,這時候 Left Arrived 也會是 false。在 Android 中則沒有負數的問題。

offset.left 又是什麼?可以看到上面參數多了 offset 這個 option,用途是,假設我們想讓 Left Arrived 在捲軸離最左邊 5px 的時候就變為 true,而不是真的完全貼到最左邊(0px)的時候才變成 true,就可以設定 offset.left = 5 來達成這個效果。有點像是留一個安全距離的感覺,對我們彼此都比較好(?)

right 的計算就比較複雜一點,先來看一張醜醜但有用的圖:
https://ithelp.ithome.com.tw/upload/images/20241005/201694194BWNfg6ycp.jpg

可見區域就是我們的 target,可以先把它想成視窗大小,左右的空白區域就是超出視窗的部分,現在假設我們把捲軸往右滾到最底,右邊那塊空白區域就會進到可見區域中(右邊空白區域沒了),但可見區域大小是固定的,所以原本在可見區域中的部分畫面也會被移到可見區域左邊的那塊空白大小(左邊空白區域變大),在捲軸滾到最右邊的情況下,clientWidth + scrollLeft 會大約等於 scrollWidth。

為什麼要減掉 ARRIVED_STATE_THRESHOLD_PIXELS(1px)?
根據 mdn 解釋,因為 scrollHeightclientHeight 都是四捨五入的數字,而 scrollTop 有可能會是浮點數,以官方範例來說:

element.scrollHeight - Math.abs(element.scrollTop) === element.clientHeight;

以上判斷不一定總是能判斷出已經捲動到最底部,所以判斷應該要給一個空間(阿縮比):

Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) <= 1;

mdn 連結:https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled

到這邊我們 leftright 的計算都看完了,不過還有有方向的問題要注意:

const { display, flexDirection } = getComputedStyle(el)

if (display === 'flex' && flexDirection === 'row-reverse') {
  arrivedState.left = right
  arrivedState.right = left
}
else {
  arrivedState.left = left
  arrivedState.right = right
}

這段滿直覺的,就是有用 flex 排版並調換方向的時候,左右狀態需要互換。
topbottom 計算概念是一樣的,就不另外寫上來了~

GitHub PR:https://github.com/RhinoLee/30days_vue/pull/20/files


今天看完 useScroll arrivedState 的部分,結果 useScroll 真的要有 part3 了 XD,明天會把剩下的 scrolling 相關功能看完(吧)


上一篇
[Day 20] useScroll - X position, Y position
下一篇
[Day 22] useScroll - scrolling
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言