【VueUse】快来看看VueUse是怎么封装EventListener

1,672 阅读7分钟

这是我开设第一个系列文章,旨在提升自己的源码阅读和调试能力、学习到更多的代码语法和逻辑。因为我是个TypeScript 新手,我会在末尾对遇到的 TS 语法进行总结,十分建议初学 TS 的同学阅读这篇文章,配合源码使用更佳。此系列文章对不懂 TS 的人员也能无障碍阅读,欢迎大家点赞收藏。 useEventListener 源码地址 useEventListener 官方文档

如何调试

  1. 克隆最新的 VueUse 仓库 git clone https://github.com/vueuse/vueuse.git
  2. 安装依赖,因为依赖管理器配置的是 pnpm,所以使用 pnpm 来安装依赖 pnpm install
  3. 启动项目 pnpm dev
  4. 如果没有意外的话,就能在浏览器上看到运行在本地的官方文档页面
  5. /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 中,来确保例如documentwindow对象是存在的,因为在Node.js环境中Dom APIs是无效的。

onMounted(() => {
  useEventListener(document, 'keydown', (e) => {
    console.log(e.key)
  })
})

参数解析

捕获.PNG

  • target - 用于注册事件监听器的目标元素。类型为MaybeRefOrGetter,并指定泛型为EventTarget;也可以为undefined。当为undefinedtarget默认为window
  • events - 事件名称,类型为StringString类型的数组。
  • listeners - 事件触发时执行的回调函数,类型为FunctionFunction类型的数组。
  • options - 设置事件处理函数的各种选项,类型为MaybeRefOrGetter,并指定泛型为booleanAddEventListenerOptions;也可以为undefinedAddEventListenerOptions类型包含以下属性:
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类型或类型为Tref或返回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中的第三个参数optionscapture表示是否在捕获阶段处理,为false时在冒泡阶段处理;once表示是否在触发事件后移除监听;passive表示是否禁止事件的默认行为;signal表示允许在事件处理中启用取消机制;当optionsBoolean时作用同capture一致。
  • Vue3 中使用watch API 监听多个值时,首个参数直接传入数组即可;第三个参数中flush属性设为'post'时,可使侦听器延迟到组件渲染之后再执行。
  • Vue3 中用于获取当前作用域的getCurrentScope函数。
  • Vue3 中用于在当前作用域注册处理回调函数,并在作用域销毁时执行的onScopeDispose函数。

在阅读源码过程中主要是学习作者的设计思想和实现技巧以及代码的可靠性和可维护性,转换一下自己敲代码的思路。如果文中有什么错误或者不理解的地方,欢迎大家在评论区中指出、讨论!!!

共同成长,无限进步!!! 💪