useEventListener:一个减少vue开发者心智负担的事件监听方法
useEventListener可以帮助我们更轻松的使用 EventListener。组件挂载后使用 addEventListener 注册事件,组件卸载时自动调用removeEventListener。那么这是怎么做到的呢?下面我们就通过阅读源码来回答这个问题。
useEventListener在vueuse中被将近60个文件用到,相当高频:
1.使用
可以在document上注册事件:
import { useEventListener } from '@vueuse/core'
useEventListener(document, 'visibilitychange', (evt) => { console.log(evt) })
如上代码所示,在文档可见度更改的时候,会执行打印的回调。可以看到使用起来蛮方便的,你不用在挂载完成时写完addEventListener 之后有在组件销毁之前写removeEventListener。你不用担心因为注册了很多事件而导致内存泄漏的问题。
还可以传递一个 ref 作为事件目标,useEventListener将会注销以前的事件,并在更改目标时注册新的事件,我们看如下的代码:
import { useEventListener } from '@vueuse/core'
const element = ref<HTMLDivElement>()
useEventListener(element, 'keydown', (e) => { console.log(e.key) })
如上代码声明了一个名为elment的ref, 使用useEventListener监听器鼠标按下事件。
<template>
<div v-if="cond" ref="element">Div1</div>
<div v-else ref="element">Div2</div>
</template>
模板代码的逻辑为如果cond条件变量为真则element引用Div1对应的dom元素;否则引用Div2。当cond变化时会触发useEventListener自动销毁旧事件和注册新事件。
2.源码
遇到超过100行代码的情况先折叠:
将源码分成如下几部分,分别介绍:
2.1参数判断和变量初始化
let target: MaybeRef<EventTarget> | undefined
let event: string
let listener: any
let options: any
if (isString(args[0])) {
[event, listener, options] = args
target = defaultWindow
}
else {
[target, event, listener, options] = args
}
if (!target)
return noop
let cleanup = noop
如果第一个参数是字符串说明第一个参数是要监听的事件名,则从args参数中解构出事件名event,监听的回调函数listener, 以及其他选项options, 关于addEventListener可以接受第三个参数options很多同学不是很熟悉,可以查看MDN文档详细了解。
如果第一个参数不是字符串则说明是要监听的目标元素target。
2.2核心逻辑
const stopWatch = watch(
() => unrefElement(target as unknown as MaybeElementRef),
(el) => {
cleanup()
if (!el)
return
el.addEventListener(event, listener, options)
cleanup = () => {
el.removeEventListener(event, listener, options)
cleanup = noop
}
},
{ immediate: true, flush: 'post' },
)
这段代码属于核心逻辑,使用了watch API , 我们结合文档对watch API三个参数的介绍来分析这段代码:
(1)watch的第一参数是要监听的数据,可以是字符串或者一个函数,这里使用的是函数。由于target要么是document要么是ref包装后的dom, 所以监听target的时候先调用unrefElement得到对应的dom。unrefElement的定义如下:
// vueuse/packages/core/unrefElement/index.ts
export function unrefElement<T extends MaybeElement>(elRef: MaybeElementRef<T>): UnRefElementReturn<T> {
const plain = unref(elRef)
return (plain as VueInstance)?.$el ?? plain
}
对参数elRef调用unref得到原始值plain,然后对plain进行判断返回plain.$el或者plain本身,这里使用到了可选链(.?)和 空值合并运算符(??)。
(2)watch的第二个参数是回调可以函数也可以是对象形式的,这里使用的是函数。回调函数中首先调用cleanup移除上一次的(之前元素的)监听。然后为新的添加事件监听并更新cleanup函数。
(3)watch的第三个参数是选项对象,有deep、immediate和flush三个选项。其中deep用于发现对象内部值的变化,也就是用于深度监听;immediate设置为true的时候可以立即触发回调, 这里设置了immediate为true从而立即触发回调,因为需要立即调用addEventListener;flush用于控制回调触发的时间,可以取pre、post和sync这三个值,这里设置flus为post, 将回调推迟到渲染之后,这样可以在回调里访问新的dom或子组件。
2.3stop方法
const stop = () => {
stopWatch()
cleanup()
}
tryOnScopeDispose(stop)
stop方法调用stopWatch()和cleanup(),停止监听和做清理工作。
tryOnScopeDispose(stop)
用于把stop()方法注册到当前活跃的effect作用域上,这个活跃的effect作用域结束之后会将stop当作回调函数调用。tryOnScopeDispose的定义如下:
// vueuse/packages/shared/tryOnScopeDispose
export function tryOnScopeDispose(fn: Fn) {
if (getCurrentScope()) {
onScopeDispose(fn)
return true
}
return false
}
这里使用到了Effect作用域API, Effect作用域是一个高阶的API, 主要用于库作者。getCurrentScope是获取当前活跃的effect作用域,如果没有则返回undefined。onScopeDispose用于在当前活跃的effect作用域上注册一个处理回调,该回调会在相关的effect作用域结束之后被调用。
可以看到useEventListener最终将stop方法作为返回值,这可以方便使用者手动调用stop方法。
3总结
本文首先介绍了useEventListener的优势,然后结合官方文档的示例介绍了其使用方法,最后分析了其源码。经过分析useEventListener的源码,我们发现其核心在于使了vue3的watchAPI和Effect作用域API。watch API的使用可以做到立即监听(调用addEventListener)和响应式监听,而Effect作用域API可以让组件卸载时自动调用removeEventListener。
4.留给读者
读完本文您可以思考如下问题:
1.useEventListener相比直接使用addEventListener和removeEventListener有什么好处?
2.addEventListener的options参数的作用是什么?
3.vue3 watch API的参数的含义?在useEventListener中使用watch的时候为什么immediate设为true?
4.useEventListener如何做到可以在组件卸载时自动调用removeEventListener?
转载自:https://juejin.cn/post/7109837557722152968