Bläddra i källkod

feat(hooks): useHoverToggle的入参refElement支持传入响应式数组 (#6333)

* feat(hooks): useHoverToggle的入参refElement支持传入响应式数组

* feat(hooks): 1、增加 useHoverToggle 中 refElement 参数关于传入响应式数组的注释说明。 2、修改 watch 监听深度,仅需浅层监听 refs 变化。 3、使用 effectScope 管理 useElementHover 实例,避免 refs 变化时事件监听器累积导致的内存泄漏问题

* feat(hooks): 在useHoverToggle中增强 updateHovers  的边界处理,优化watch方案,只监听元素数量变化而不是整个数组变化,避免过度依赖收集

---------

Co-authored-by: xiaobin <xiaobin_chen@fzzixun.com>
broBinChen 2 månader sedan
förälder
incheckning
e7fd0e3b6a
1 ändrade filer med 57 tillägg och 15 borttagningar
  1. 57 15
      packages/effects/hooks/src/use-hover-toggle.ts

+ 57 - 15
packages/effects/hooks/src/use-hover-toggle.ts

@@ -2,7 +2,7 @@ import type { Arrayable, MaybeElementRef } from '@vueuse/core';
 
 import type { Ref } from 'vue';
 
-import { computed, onUnmounted, ref, unref, watch } from 'vue';
+import { computed, effectScope, onUnmounted, ref, unref, watch } from 'vue';
 
 import { isFunction } from '@vben/utils';
 
@@ -20,12 +20,12 @@ const DEFAULT_ENTER_DELAY = 0; // 鼠标进入延迟时间,默认为 0(立
 
 /**
  * 监测鼠标是否在元素内部,如果在元素内部则返回 true,否则返回 false
- * @param refElement 所有需要检测的元素。如果提供了一个数组,那么鼠标在任何一个元素内部都会返回 true
+ * @param refElement 所有需要检测的元素。支持单个元素、元素数组或响应式引用的元素数组。如果鼠标在任何一个元素内部都会返回 true
  * @param delay 延迟更新状态的时间,可以是数字或包含进入/离开延迟的配置对象
  * @returns 返回一个数组,第一个元素是一个 ref,表示鼠标是否在元素内部,第二个元素是一个控制器,可以通过 enable 和 disable 方法来控制监听器的启用和禁用
  */
 export function useHoverToggle(
-  refElement: Arrayable<MaybeElementRef>,
+  refElement: Arrayable<MaybeElementRef> | Ref<HTMLElement[] | null>,
   delay: (() => number) | HoverDelayOptions | number = DEFAULT_LEAVE_DELAY,
 ) {
   // 兼容旧版本API
@@ -38,20 +38,58 @@ export function useHoverToggle(
           ...delay,
         };
 
-  const isHovers: Array<Ref<boolean>> = [];
   const value = ref(false);
   const enterTimer = ref<ReturnType<typeof setTimeout> | undefined>();
   const leaveTimer = ref<ReturnType<typeof setTimeout> | undefined>();
-  const refs = Array.isArray(refElement) ? refElement : [refElement];
-  refs.forEach((refEle) => {
-    const eleRef = computed(() => {
-      const ele = unref(refEle);
-      return ele instanceof Element ? ele : (ele?.$el as Element);
+  const hoverScopes = ref<ReturnType<typeof effectScope>[]>([]);
+
+  // 使用计算属性包装 refElement,使其响应式变化
+  const refs = computed(() => {
+    const raw = unref(refElement);
+    if (raw === null) return [];
+    return Array.isArray(raw) ? raw : [raw];
+  });
+  // 存储所有 hover 状态
+  const isHovers = ref<Array<Ref<boolean>>>([]);
+
+  // 更新 hover 监听的函数
+  function updateHovers() {
+    // 停止并清理之前的作用域
+    hoverScopes.value.forEach((scope) => scope.stop());
+    hoverScopes.value = [];
+
+    isHovers.value = refs.value.map((refEle) => {
+      if (!refEle) {
+        return ref(false);
+      }
+      const eleRef = computed(() => {
+        const ele = unref(refEle);
+        return ele instanceof Element ? ele : (ele?.$el as Element);
+      });
+
+      // 为每个元素创建独立的作用域
+      const scope = effectScope();
+      const hoverRef = scope.run(() => useElementHover(eleRef)) || ref(false);
+      hoverScopes.value.push(scope);
+
+      return hoverRef;
     });
-    const isHover = useElementHover(eleRef);
-    isHovers.push(isHover);
+  }
+
+  // 监听元素数量变化,避免过度执行
+  const elementsCount = computed(() => {
+    const raw = unref(refElement);
+    if (raw === null) return 0;
+    return Array.isArray(raw) ? raw.length : 1;
   });
-  const isOutsideAll = computed(() => isHovers.every((v) => !v.value));
+
+  // 初始设置
+  updateHovers();
+
+  // 只在元素数量变化时重新设置监听器
+  const stopWatcher = watch(elementsCount, updateHovers, { deep: false });
+
+  const isOutsideAll = computed(() => isHovers.value.every((v) => !v.value));
 
   function clearTimers() {
     if (enterTimer.value) {
@@ -96,7 +134,7 @@ export function useHoverToggle(
     }
   }
 
-  const watcher = watch(
+  const hoverWatcher = watch(
     isOutsideAll,
     (val) => {
       setValueDelay(!val);
@@ -106,15 +144,19 @@ export function useHoverToggle(
 
   const controller = {
     enable() {
-      watcher.resume();
+      hoverWatcher.resume();
     },
     disable() {
-      watcher.pause();
+      hoverWatcher.pause();
     },
   };
 
   onUnmounted(() => {
     clearTimers();
+    // 停止监听器
+    stopWatcher();
+    // 停止所有剩余的作用域
+    hoverScopes.value.forEach((scope) => scope.stop());
   });
 
   return [value, controller] as [typeof value, typeof controller];