useEventListener - VueUse 源码解读

1、作用

轻松使用EventListener,在挂载时使用addEventListener注册,在卸载时自动删除EventListener。

2、使用案例

未使用

我们在做事件监听时,往往需要手动注册,并且在组件卸载时,删除事件; 例如:监听页面聚焦时更新数据

js 复制代码
const visibilitychangeFn = () => {
  // document.hidden为false表示视图页面进入了前台
  const pageVisibility = document.visibilityState;
  if (pageVisibility === 'visible') {
    init();
  }
};

// 监听页面聚焦
onMounted(() => {
  document.addEventListener('visibilitychange', visibilitychangeFn);
});

// 监听页面聚焦
onUnmounted(() => {
  document.removeEventListener('visibilitychange', visibilitychangeFn);
});

使用useEventListener

js 复制代码
const visibilitychangeFn = () => {
  // document.hidden为false表示视图页面进入了前台
  const pageVisibility = document.visibilityState;
  if (pageVisibility === 'visible') {
    console.log('page-visible, init')
  }
};

const stopListen = useEventListener('visibilitychange', visibilitychangeFn)

// 有需要时,你也可以手动取消监听
stopListen()

3、源码解读

1、使用多个函数重载实现支持多种参数 2、核心在于使用了 onScopeDispose 这个vue提供的api

这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的 vue官方文档 onScopeDispose

js 复制代码
import type { Arrayable, MaybeRefOrGetter, Fn, AnyFn, MaybeElementRef } from '../utils' 
import { noop, isObject, tryOnScopeDispose } from '../utils' 
import { watch } from 'vue'
import { unrefElement } from '../unrefElement'
import { toValue } from '../toValue'


interface InferEventTarget<Events> {
  addEventListener: (event: Events, fn?: any, options?: any) => any
  removeEventListener: (event: Events, fn?: any, options?: any) => any
}

export interface GeneralEventListener<E = Event> {
  (evt: E): void
}

/**
 * 重载1: 省略了,window目标
 */
export function useEventListener<E extends keyof WindowEventMap>(
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 


/**
 * 重载 2: 明确是 Window作为监听目标
 */
export function useEventListener<E extends keyof WindowEventMap>(
  target: Window,
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 

/**
 * 重载 3: 明确是 Document作为监听目标
 */
export function useEventListener<E extends keyof DocumentEventMap>(
  target: DocumentOrShadowRoot,
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: DocumentEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 


/**
 * 重载 4: 明确是 HTMLElement 作为监听目标
 */
export function useEventListener<E extends keyof HTMLElementEventMap>(
  target: MaybeRefOrGetter<HTMLElement | null | undefined>,
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: HTMLElementEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 


/**
 * 重载 5: 自定义事件和监听目标(通过事件来推断target的类型)
 * EventType 是泛型,允许传入 任何 事件类型
 */
export function useEventListener<Names extends string, EventType = Event>(
  target: MaybeRefOrGetter<InferEventTarget<Names> | null | undefined>,
  event: Arrayable<Names>,
  listener: Arrayable<GeneralEventListener<EventType>>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn


/**
 * 重载 6: 自定义事件降级(不推断target的类型)
 */
export function useEventListener<EventType = Event>(
  target: MaybeRefOrGetter<EventTarget | null | undefined>,
  event: Arrayable<string>,
  listener: Arrayable<GeneralEventListener<EventType>>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn


export function useEventListener(...args: any[]) {
  let target: MaybeRefOrGetter<EventTarget> | undefined
  let events: Arrayable<string>
  let listeners: Arrayable<AnyFn>
  let options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined
  
  // 第一个参数是 事件类型
  if (typeof args[0] === 'string' || Array.isArray(args[0])) {
    [events, listeners, options] = args
    target = window
  }
  else { // 第一个参数是 监听目标
    [target, events, listeners, options] = args
  }

  // 无监听目标,无操作
  if(!target) return noop;

  // 兼容事件和响应是 数组的情况
  if (!Array.isArray(events))
    events = [events]
  if (!Array.isArray(listeners))
    listeners = [listeners]

  const cleanups: AnyFn[] = []
  const cleanup = () => {
    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)
  }


  const stopWatch = watch(
    ()=> [unrefElement(target as unknown as MaybeElementRef), toValue(options)],
    ([el, options]) => {
      cleanup()
      if (!el)
        return

      // 创建options的克隆,以避免在删除时被更改
      const optionsClone = isObject(options) ? { ...options } : options
      
      cleanups.push(
        ...(events as string[]).flatMap((event) => {
          return (listeners as AnyFn[]).map(listener => register(el, event, listener, optionsClone))
        })
      )
    }
  )

  const stop = () => {
    stopWatch()
    cleanup()
  }

  tryOnScopeDispose(stop)

  return stop
}

4、了解 effectScope、getCurrentScope、onScopeDispose

这些api,我们的日常开发基本上使用不上,做了解即可;

  • onScopeDispose

在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的

这里截取,某乎上,算是大白话,容易理解的一个评论

如何理解Vue3里的EffectScope?

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅3 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax