iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0
自我挑戰組

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

[Day 9] useEventListener - 主要流程

  • 分享至 

  • xImage
  •  

昨天講了 useEventListener 可以傳入的參數,以及 tryOnScopeDispose 如何做到在組件註銷前,執行我們傳給 tryOnScopeDispose 的 cleanup function,今天來講整個 useEventListener 的運作流程~

流程起點 - watch

// src/compositions/useEventListener.js
export function useEventListener(target, event, listener, options) {
  // ...略

  // 用來收集 removeEventListener function
  const cleanups = []
  const cleanup = () => {
    cleanups.forEach(cleanup => cleanup())
    cleanups.length = 0
  }

  const register = (el, event, listener, options) => {
    el.addEventListener(event, listener, options)
    return () => el.removeEventListener(event, listener, options)
  }

  const stopWatch = watch(
    () => [unrefElement(target), toValue(_options)],
    ([el, options]) => {
      cleanup()
      if (!el)
        return

      const optionsClone = isObject(options) ? { ...options } : options
      cleanups.push(...events.flatMap((event) => {
        return listeners.map(listener => register(el, event, listener, optionsClone))
      }))
    },
    { immediate: true, flush: 'post' },
  )

  const stop = () => {
    stopWatch()
    cleanup()
  }

  tryOnScopeDispose(stop)

  return stop
};

因為 watch 有 immediate: true option,所以整個流程的起點就是這個 watch 的 callback, target 跟 options 是這個 watch 觀察的對象,target 跟 options 參數可以參考昨天的參數介紹,因為 target 會是 DOM,可以看到 watch 有設定另一個 option flush: 'post',因為 Vue 針對效能優化的關係,一般來說 watch callback 會被批次處理,避免重複呼叫,這邊設定 flush: 'post' 簡單來說就是可以在 watcher callback 拿到當前組件最新的 DOM 狀態,詳細說明可以參考:https://vuejs.org/guide/essentials/watchers.html#callback-flush-timing

也因為 target 可以是 ref 的關係,所以這邊有做一個 unrefElement,以下為相關實作

// src/helper.js
export function toValue(r) {
  return typeof r === 'function'
    ? r()
    : unref(r)
}

export function unrefElement(elRef) {
  const plain = toValue(elRef)
  // 有 $el 的話是 vue component instance
  return plain?.$el ?? plain
}

接著我們看到每次 watch callback 被呼叫時,會先執行一次 cleanup(),這個後面再說,先記住有這件事就好 XD
先繼續往下看,optionsClone 是在 options 是物件的時候做淺拷貝,isObject 的原始碼我覺得滿有趣的

export function isObject(val) {
  return toString.call(val) === '[object Object]'
}

可以到 chrome console 面板貼上一下這段試試

console.log(Object.prototype.toString.call([]));        // '[object Array]'
console.log(Object.prototype.toString.call({}));        // '[object Object]'
console.log(Object.prototype.toString.call(null));      // '[object Null]'
console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]'
console.log(Object.prototype.toString.call(42));        // '[object Number]'
console.log(Object.prototype.toString.call('string'));  // '[object String]'
console.log(Object.prototype.toString.call(true));      // '[object Boolean]'
console.log(Object.prototype.toString.call(() => {}));  // '[object Function]'

register & cleanup

接著這段就是 useEventListener 的重頭戲了,所以先把相關程式碼整理到這邊

// src/compositions/useEventListener.js
export function useEventListener(target, event, listener, options) {
  // ...略

  // 用來收集 removeEventListener function
  const cleanups = []
  const cleanup = () => {
    cleanups.forEach(cleanup => cleanup())
    cleanups.length = 0
  }

  const register = (el, event, listener, options) => {
    el.addEventListener(event, listener, options)
    return () => el.removeEventListener(event, listener, options)
  }

  const stopWatch = watch(
    () => [unrefElement(target), toValue(_options)],
    ([el, options]) => {
      // ...略
      cleanups.push(...events.flatMap((event) => {
        return listeners.map(listener => register(el, event, listener, optionsClone))
      }))
    // ...略
  )

  const stop = () => {
    stopWatch()
    cleanup()
  }

  tryOnScopeDispose(stop)

  return stop
};

假設我們參數是:

  • events = ['click', 'mouseover']
  • listeners = [onClickFn, onHoverFn]

執行過程會是:

  1. 對於 'click' 事件註冊 onClickFn
  2. 對於 'click' 事件註冊 onHoverFn
  3. 對於 'mouseover' 事件註冊 onClickFn
  4. 對於 'mouseover' 事件註冊 onHoverFn

最終結果:

  • 總共呼叫了 4 次 register 函數,針對 'click', 'mouseover' 事件分別都註冊了 onClickFn, onHoverFn
  • return 4 個對應的 cleanup function,這些函數被扁平化到一個 Array 中並 push 到 cleanups Array

flatMap 的作用:

  • 如果我們只使用 map,結果會是一個二維陣列:[[cleanup1, cleanup2], [cleanup3, cleanup4]]
  • 使用 flatMap 將這個結果轉換為一維陣列:[cleanup1, cleanup2, cleanup3, cleanup4]

先前有提到過每次 watch callback 執行的時候,會先執行一次 cleanup(),現在就比較清楚 cleanup() 具體來說是在執行我們在 register 的同時,return 的那些 removeEventListener。


今天把 useEventListener 的主要流程都講完了,覺得 register 跟 cleanups 的相關設計滿巧妙的,
明天會接著講 useEventListener 在原始碼中的 unit test 相關實作~


上一篇
[Day 8] useEventListener - 參數介紹 & 核心 tryOnScopeDispose
下一篇
[Day 10] useEventListener - unit test
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言