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?

相关推荐
A242073493025 分钟前
JavaScript学习
前端·javascript·学习
奋斗吧程序媛27 分钟前
动态组件驱动的标签页架构(简单来说:一个页面包含许多Tabs页面,这些Tabs页面渲染逻辑)
前端·javascript·vue.js
Felix_Fly28 分钟前
用 Vue3 + naive-cron 开发 Cron 表达式工具:从 0 到 1 实现生成 + 反解析
前端·javascript·vue.js·vue·cron·naive
开发者小天31 分钟前
react中useReducer的使用
前端·javascript·react.js
小虎牙00736 分钟前
关于Android Compose架构的思考
android·前端·mvvm
Calm5502 小时前
ele表单未输入值提示为英文
前端
爪洼守门员2 小时前
前端性能优化
开发语言·前端·javascript·笔记·性能优化
TOYOAUTOMATON2 小时前
GTH系列模组介绍
前端·目标检测·自动化
2022.11.7始学前端2 小时前
n8n第十节 把Markdown格式的会议纪要发到企微
前端·chrome·n8n