这是我开设第一个系列文章,旨在提升自己的源码阅读和调试能力、学习到更多的代码语法和逻辑。因为我是个TypeScript 新手,我会在末尾对遇到的 TS 语法进行总结,十分建议初学 TS 的同学阅读这篇文章,配合源码使用更佳。此系列文章对不懂 TS 的人员也能无障碍阅读,欢迎大家点赞收藏。 useEventListener 源码地址 useEventListener 官方文档
如何调试
- 克隆最新的 VueUse 仓库
git clone https://github.com/vueuse/vueuse.git
- 安装依赖,因为依赖管理器配置的是
pnpm
,所以使用pnpm
来安装依赖pnpm install
- 启动项目
pnpm dev
- 如果没有意外的话,就能在浏览器上看到运行在本地的官方文档页面
- 在
/packages/core
文件夹下找到想要调试的目标方法,在 VSCode 和浏览器源代码中设置断点
接下来就可以开始愉快的调试了!
系列文章
useEventListener
用途:轻松使用EventListener
,元素挂载时使用addEventListener
注册事件监听器,元素卸载时自动使用removeEventListener
注销事件监听器。
使用方法
import { useEventListener } from '@vueuse/core'
// 基本使用方法
useEventListener(document, 'visibilitychange', (evt) => {
console.log(evt)
})
// 不指定事件目标元素
// 当不指定目标元素时,默认为 window
useEventListener('visibilitychange', (evt) => {
console.log(evt)
})
// 传递 ref 来指定事件目标元素
const element = ref<HTMLDivElement>()
useEventListener(element, 'keydown', (e) => {
console.log(e.key)
})
// 调用返回函数来注销事件监听器
const cleanup = useEventListener(document, 'keydown', (e) => {
console.log(e.key)
})
cleanup()
// 一次注册多个事件监听器
useEventListener(document, ['mousedown', 'mouseup'], (e) => {
console.log(e)
})
// 配置事件触发时执行多个回调函数
useEventListener(document, 'mousedown', [
(e) => {
console.log('event1', e);
},
(e) => {
console.log('event2', e);
}]
)
// 注册多个事件监听器以及多个回调函数
// 需要注意的是只有任意一个事件触发时,所有回调函数都会执行,并不是一一对应
useEventListener(document, ['mousedown', 'mouseup'], [
(e) => {
console.log('event1', e);
},
(e) => {
console.log('event2', e);
}]
)
当项目为服务端渲染(SSR)时,需要将onMounted
hook 中,来确保例如document
和window
对象是存在的,因为在Node.js
环境中Dom APIs
是无效的。
onMounted(() => {
useEventListener(document, 'keydown', (e) => {
console.log(e.key)
})
})
参数解析
- target - 用于注册事件监听器的目标元素。类型为
MaybeRefOrGetter
,并指定泛型为EventTarget
;也可以为undefined
。当为undefined
时target
默认为window
。 - events - 事件名称,类型为
String
或String
类型的数组。 - listeners - 事件触发时执行的回调函数,类型为
Function
或Function
类型的数组。 - options - 设置事件处理函数的各种选项,类型为
MaybeRefOrGetter
,并指定泛型为boolean
或AddEventListenerOptions
;也可以为undefined
。AddEventListenerOptions
类型包含以下属性:
interface AddEventListenerOptions {
// 表示事件是否在捕获阶段被处理,默认为 false,即表示事件在冒泡阶段被处理
capture?: boolean;
// 表示事件触发回调函数执行后被自动移除,默认为 false,为 true 时事件只会触发一次
once?: boolean;
// 表示是否禁止事件的默认行为。如果确定事件处理程序不会调用preventDefault(),则可以将此选项设置为true,默认为false。
passive?: boolean;
// 用于允许在事件处理中启用取消机制,通过调用 AbortSignal 对象的 abort() 方法来取消正在进行的操作。
signal?: AbortSignal;
}
当 options 为Boolean
类型时,就视为和AddEventListenerOptions
中的capture
属性用途一致。
❗️PS:关于MaybeRefOrGetter<T>
类型,VueUse 中用得很多,可以去看看源码,很好理解,表示为T
类型或类型为T
的ref
或返回T
类型的函数,T
是一个泛型参数,表示一种类型,根据实际使用场景的需要指定T
的具体类型。
主要逻辑
// 这需要用到的一些常量,从其他文件复制过来,方便查看
const isClient = typeof window !== 'undefined'
const defaultWindow = isClient ? window : undefined
const noop = () => {}
export function useEventListener(...args: any[]) {
let target: MaybeRefOrGetter<EventTarget> | undefined
let events: Arrayable<string>
let listeners: Arrayable<Function>
let options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined
// 没有指定目标元素 target
if (typeof args[0] === 'string' || Array.isArray(args[0])) {
// args 解构赋值
[events, listeners, options] = args
// 将 target 默认设为 window 对象
target = defaultWindow
}
else {
// args 解构赋值
[target, events, listeners, options] = args
}
// 当时传入的 target 为无效值时,直接返回一个空函数
if (!target)
return noop
// 将非数组类型的 events 和 listeners 转为数组类型
if (!Array.isArray(events))
events = [events]
if (!Array.isArray(listeners))
listeners = [listeners]
// 用于存储每个事件监听器的移除函数
const cleanups: Function[] = []
// 用于移除所有的事件监听器
const cleanup = () => {
// 执行 cleanups 中的所有移除事件监听器函数
cleanups.forEach(fn => fn())
cleanups.length = 0
}
// 用于将事件监听器注册到目标元素上,并返回用于移除事件监听器的函数
const register = (el: any, event: string, listener: any, options: any) => {
el.addEventListener(event, listener, options)
return () => el.removeEventListener(event, listener, options)
}
// 监听 target 和 options 变化,并在发生变化时时移除旧的事件监听器,注册新的监听器
const stopWatch = watch(
// unrefElement 在下面介绍
() => [unrefElement(target as unknown as MaybeElementRef), toValue(options)],
// 发生变化时的回调函数
([el, options]) => {
// 清除旧的事件监听器
cleanup()
// 没有传入目标元素直接返回
if (!el)
return
// 1. 将 events 事件名称进行扁平化(一级)后遍历
// 2. 在 events 遍历中再将 listeners 事件回调函数数组进行遍历
// 3. 在遍历 listeners 中调用 register 方法把所有的回调函数注册到每一个event事件的回调函数
// 4. 返回用于移除事件监听器函数
// 5. 将移除事件监听器函数 push 到 cleanups
cleanups.push(
...(events as string[]).flatMap((event) => {
return (listeners as Function[]).map(listener => register(el, event, listener, options))
}),
)
},
// 创建侦听器时立即触发回调,flush 设为 'post',表示使侦听器延迟到组件渲染之后再执行
{ immediate: true, flush: 'post' },
)
// 用于停止 Watch 数据侦听和移除事件监听器
const stop = () => {
// 停止 Watch 数据侦听
stopWatch()
// 移除所有的事件监听器
cleanup()
}
// tryOnScopeDispose 在下面介绍
// 简单来说就是作用域销毁时执行 stop 函数
tryOnScopeDispose(stop)
// 返回用于停止数据侦听器和移除事件监听器的函数
return stop
}
- 关于
unrefElement()
方法,具体功能是:传入ref
引用的HTML
元素或Vue
组件,返回元素本身。
export function unrefElement<T extends MaybeElement>(elRef: MaybeComputedElementRef<T>): UnRefElementReturn<T> {
// 获取 ref 的 value
const plain = toValue(elRef)
// 如果是 Vue 实例,则返回实例上的 $el 属性,否则直接返回 ref 的 value
return (plain as VueInstance)?.$el ?? plain
}
- 关于
tryOnScopeDispose()
import { getCurrentScope, onScopeDispose } from 'vue-demi'
export function tryOnScopeDispose(fn: Fn) {
// 获取当前的作用域
if (getCurrentScope()) {
// 在作用域销毁时调用回调函数
onScopeDispose(fn)
return true
}
return false
}
getCurrentScope - 获取当前活跃的 effect 作用域 ,若没有时返回undefined
。
onScopeDispose - 在当前活跃的 effect 作用域 上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
总结
addEventListener
中的第三个参数options
,capture
表示是否在捕获阶段处理,为false
时在冒泡阶段处理;once
表示是否在触发事件后移除监听;passive
表示是否禁止事件的默认行为;signal
表示允许在事件处理中启用取消机制;当options
为Boolean
时作用同capture
一致。- Vue3 中使用
watch
API 监听多个值时,首个参数直接传入数组即可;第三个参数中flush
属性设为'post'
时,可使侦听器延迟到组件渲染之后再执行。 - Vue3 中用于获取当前作用域的
getCurrentScope
函数。 - Vue3 中用于在当前作用域注册处理回调函数,并在作用域销毁时执行的
onScopeDispose
函数。
在阅读源码过程中主要是学习作者的设计思想和实现技巧以及代码的可靠性和可维护性,转换一下自己敲代码的思路。如果文中有什么错误或者不理解的地方,欢迎大家在评论区中指出、讨论!!!
共同成长,无限进步!!! 💪