昨天講了 useEventListener 可以傳入的參數,以及 tryOnScopeDispose 如何做到在組件註銷前,執行我們傳給 tryOnScopeDispose 的 cleanup function,今天來講整個 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]) => {
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]'
接著這段就是 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]
執行過程會是:
最終結果:
register
函數,針對 'click', 'mouseover' 事件分別都註冊了 onClickFn, onHoverFncleanups
ArrayflatMap 的作用:
[[cleanup1, cleanup2], [cleanup3, cleanup4]]
[cleanup1, cleanup2, cleanup3, cleanup4]
先前有提到過每次 watch callback 執行的時候,會先執行一次 cleanup()
,現在就比較清楚 cleanup()
具體來說是在執行我們在 register
的同時,return 的那些 removeEventListener。
今天把 useEventListener 的主要流程都講完了,覺得 register 跟 cleanups 的相關設計滿巧妙的,
明天會接著講 useEventListener 在原始碼中的 unit test 相關實作~