likes
comments
collection
share

useEventListener:一个减少vue开发者心智负担的事件监听方法

作者站长头像
站长
· 阅读数 16

useEventListener可以帮助我们更轻松的使用 EventListener。组件挂载后使用 addEventListener 注册事件,组件卸载时自动调用removeEventListener。那么这是怎么做到的呢?下面我们就通过阅读源码来回答这个问题。

useEventListener在vueuse中被将近60个文件用到,相当高频:

useEventListener:一个减少vue开发者心智负担的事件监听方法

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行代码的情况先折叠:

useEventListener:一个减少vue开发者心智负担的事件监听方法

将源码分成如下几部分,分别介绍:

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?