引言
在日常开发中,我们经常会遇到需要使用事件监听器来处理各种场景的情况。然而,随着应用复杂度的增加,事件监听的管理变得愈发复杂.常见的问题包括忘记在组件卸载时移除监听器,容易导致内存泄漏,以及在需要同时监听多个事件或一个事件触发多个处理函数时,代码的冗余和可维护性问题。为了解决这些问题,我深入研究学习了 vueuse-useEventListener 的实现,并结合自己项目的具体需求,实现了一个定制化的 useEventListener Hook.
实际场景代码优化示例
在之前实现的签名组件中,存在大量重复且冗余的事件监听注册代码,如下所示:
onMounted(() => {
drawSignaturePad();
signaturePad.value?.addEventListener("mousedown", handleStart);
signaturePad.value?.addEventListener("mousemove", handleMove);
signaturePad.value?.addEventListener("mouseup", handleEnd);
signaturePad.value?.addEventListener("mouseleave", handleEnd);
signaturePad.value?.addEventListener("touchstart", handleStart, {
passive: true,
});
signaturePad.value?.addEventListener("touchmove", handleMove, {
passive: true,
});
signaturePad.value?.addEventListener("touchend", handleEnd);
});
onBeforeUnmount(() => {
signaturePad.value?.removeEventListener("mousedown", handleStart);
signaturePad.value?.removeEventListener("mousemove", handleMove);
signaturePad.value?.removeEventListener("mouseup", handleEnd);
signaturePad.value?.removeEventListener("mouseleave", handleEnd);
signaturePad.value?.removeEventListener("touchstart", handleStart);
signaturePad.value?.removeEventListener("touchmove", handleMove);
signaturePad.value?.removeEventListener("touchend", handleEnd);
});
通过引入 useEventListener Hook,代码得到了显著的简化和优化:
useEventListener(
signaturePad,
["mousedown", "mousemove"],
[[handleStart], [handleMove]],
);
useEventListener(
signaturePad,
["touchstart", "touchmove"],
[[handleStart], [handleMove]],
{
passive: true,
},
);
useEventListener(
signaturePad,
["mouseup", "mouseleave", "touchend"],
handleEnd,
);
onMounted(() => {
drawSignaturePad();
});
这种优化不仅减少了代码的冗余,提高了可读性和可维护性,还降低了因手动管理事件监听器而引发的错误风险,使得签名组件的事件处理逻辑更加清晰和高效.
技术实现分析
根据MDN-addEventListener的文档,注册事件监听器需要以下属性参数:
target(触发事件的对象)、type(事件类型)、`- listener`(事件监听函数)
options(事件监听的其他配置参数)。
vueuse-useEventListener对这些属性参数进行了良好的类型定义,并通过函数重载来支持不同类型的函数签名.
在常规开发中,我们通常会在onMounted和onBeforeUnmount生命周期钩子中分别注册和移除事件监听器。然而,在复杂的组件中,如果使用v-if等指令动态控制组件的显示和隐藏,可能会在移除事件监听器时遗漏某些情况,从而导致内存泄漏或其他副作用.为了解决这一问题,vueuse-useEventListener采用了一种更为灵活的实现方式:通过watch来监听target事件对象的更新,自动注册和移除事件监听器,从而及时清除副作用.此外,它还利用了Vue的getCurrentScope和onScopeDisposeAPI,确保在effect作用域销毁时能够彻底清除副作用,并提供了一个stop函数,允许用户自定义清除副作用的时机,进一步增强了灵活性和可控性.
定制化 useEventListener 的需求分析
站在 vueuse-useEventListener 开发大佬的肩头,根据自身项目的特点和需求,对 useEventListener 进行定制化改进,以更好地满足我们的开发场景,有如下需求:
events&listeners的数组能不能1对1绑定注册
// 伪代码
useEventListener(
signaturePad,
["mousedown", "mousemove"],
[handleStart, handleMove],
undefined, // AddEventListenerOptions
true // 开启一一对应
);
// mousedown - handleStart
// mousemove - handleMove
listeners是不是可以定义成元组,实现每个listener 可自定义options
// 伪代码
useEventListener(
signaturePad,
["mousedown", "mousemove"],
[[handleStart, { passive: true }], [handleMove, true]],
undefined, // listenerOptions
true // 开启一一对应
);
代码实现
类型声明
import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter } from "vue";
import { getCurrentScope, onScopeDispose, toValue, watch } from "vue";
type EventListenerTarget = MaybeRefOrGetter<
EventTarget | HTMLElement | Window | Document | null | undefined
>;
type ArrayAble<T> = T[] | T;
type Events =
| ArrayAble<string>
| ArrayAble<keyof WindowEventMap>
| ArrayAble<keyof DocumentEventMap>;
type Listeners = ArrayAble<EventListenerOrEventListenerObject>;
type ListenerAndOptions = [
EventListenerOrEventListenerObject,
(boolean | AddEventListenerOptions)?,
];
export default function useEventListener(
target: EventListenerTarget,
events: Events,
listeners: ListenerAndOptions[] | Listeners,
options?: boolean | AddEventListenerOptions,
mapping?: boolean,
): () => void
具体实现
export default function useEventListener(
target: EventListenerTarget,
events: Events,
listeners: ListenerAndOptions[] | Listeners,
options?: boolean | AddEventListenerOptions,
mapping?: boolean,
) {
if (!target) return () => {};
events = Array.isArray(events) ? events : [events];
listeners = Array.isArray(listeners) ? listeners : [listeners];
listeners = listeners.map((listener) => {
if (Array.isArray(listener)) {
mapping = mapping ?? true;
return [listener[0], listener[1] ?? options];
} else {
mapping = mapping ?? false;
return [listener, options];
}
}) as ListenerAndOptions[];
let cleanups: (() => void)[] = [];
const cleanup = () => {
cleanups.forEach((fn) => fn());
cleanups = [];
};
const register = (
target: EventTarget | HTMLElement | Window | Document,
event: keyof WindowEventMap | keyof DocumentEventMap | string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
) => {
target.addEventListener(event, listener, options);
return () => target.removeEventListener(event, listener, options);
};
const stopWatch = watch(
() =>
(
toValue(
target as unknown as MaybeRef<
| HTMLElement
| SVGElement
| ComponentPublicInstance
| undefined
| null
>,
) as ComponentPublicInstance
)?.$el ?? toValue(target),
(el) => {
cleanup();
if (!el) return;
cleanups.push(
...events.flatMap((event, i) => {
if (!mapping) {
return listeners.map((listener) =>
register(el, event, ...(listener as ListenerAndOptions)),
);
}
return [register(el, event, ...(listeners[i] as ListenerAndOptions))];
}),
);
},
{ immediate: true, flush: "post" },
);
const stop = () => {
stopWatch();
cleanup();
};
if (getCurrentScope()) {
onScopeDispose(stop);
}
return stop;
}
用法
以下是使用 useEventListener 函数的多种不同场景及示例代码。在这些示例中,假设你已经正确导入了 useEventListener 函数,如下所示:
import { useEventListener } from "learnDoUI";
给一个 event 添加一个 listener
给 document 对象的 click 事件添加一个简单的监听器
const handleClick = (evt: Event) => {
console.log(evt)
}
useEventListener(document, "click", handleClick);
// 当 click 事件被触发时,handleClick 函数将被调用,并且事件对象将作为参数传递给它。
给一个 event 添加多个 listener
为一个事件添加多个监听器,可以将这些监听器作为数组传递给 useEventListener 函数
const handleClick = (evt: Event) => {
console.log(evt, "click handler 1");
};
const handleClick1 = (evt: Event) => {
console.log(evt, "click handler 2");
};
useEventListener(document, "click", [handleClick, handleClick1]);
// 当 click 事件触发时,handleClick 和 handleClick1 函数将依次被调用,并且都将接收到事件对象作为参数。
给一个 event 添加一个 listener 且设置 listenerOptions
为单个监听器设置监听器选项,例如 capture、once、passive 等
const handleClick = (evt: Event) => {
console.log(evt, "click");
};
useEventListener(document, "click", handleClick, { capture: true });
// 将 handleClick 监听器添加到 click 事件,并将 capture 选项设置为 true。这意味着事件将在捕获阶段触发该监听器
给一个 event 添加多个 listener 且设置相同 listenerOptions
为多个监听器设置相同的监听器选项
const handleClick = (evt: Event) => {
console.log(evt, "click handler 1");
};
const handleClick1 = (evt: Event) => {
console.log(evt, "click handler 2");
};
useEventListener(document, "click", [handleClick, handleClick1], { passive: true });
// handleClick 和 handleClick1 都将在 click 事件触发时被调用,并且它们都将使用 passive 选项,该选项可以提高性能,特别是在滚动等操作中。
给一个 event 添加多个 listener 且设置不同 listenerOptions
为多个监听器设置不同的监听器选项时,可以将监听器和选项作为元组传递
const handleClick = (evt: Event) => {
console.log(evt, "click handler 1");
};
const handleClick1 = (evt: Event) => {
console.log(evt, "click handler 2");
};
useEventListener(document, "click", [[handleClick, { capture: true }], [handleClick1, { once: true }]], undefined, false);
// handleClick 监听器将使用 capture 选项,而 handleClick1 监听器将使用 once 选项,意味着它只会被调用一次。
给多个 event 添加一个相同 listener
为多个事件添加相同的监听器,可以将事件列表作为数组传递
const handleEnd = (evt: Event) => {
console.log(evt, "end event handler");
};
useEventListener(document, ["mouseup", "mouseleave"], handleEnd);
// 当 mouseup 或 mouseleave 事件触发时,handleEnd 函数将被调用
给多个 event 添加多个相同 listener
为多个事件添加多个相同的监听器,你可以将监听器列表和事件列表都作为数组传递。
const handleEnd = (evt: Event) => {
console.log(evt, "end event handler 1");
};
const handleEnd1 = (evt: Event) => {
console.log(evt, "end event handler 2");
};
useEventListener(document, ["mouseup", "mouseleave"], [handleEnd, handleEnd1]);
// 当 mouseup 或 mouseleave 事件触发时,handleEnd 和 handleEnd1 函数将依次被调用。
多个 event 一一对应 listener
对于多个事件和多个监听器,可以使用以下方式来实现一一对应关系。
- 将监听器作为元组传递,默认是一一对应关系绑定
作为元组传递且不想一一对应关系, 需要设置第四个参数为 false, 对于 listenerOptions(第三个参数) 不需要设置时要用 undefined 占位
const handleMouseup = (evt: Event) => {
console.log(evt, "mouseup handler");
};
const handleMouseleave = (evt: Event) => {
console.log(evt, "mouseleave handler");
};
useEventListener(document, ["mouseup", "mouseleave"], [[handleMouseup], [handleMouseleave]]);
// useEventListener(document, ["mouseup", "mouseleave"], [[handleMouseup], [handleMouseleave]], undefined, false);
- 监听器作为数组传递, 则需要第四个参数为 true, 对于 listenerOptions(第三个参数) 不需要设置时要用 undefined 占位
const handleMouseup = (evt: Event) => {
console.log(evt, "mouseup handler");
};
const handleMouseleave = (evt: Event) => {
console.log(evt, "mouseleave handler");
};
useEventListener(document, ["mouseup", "mouseleave"], [handleMouseup, handleMouseleave], undefined, true);
清理监听器
useEventListener 函数会返回一个清理函数,你可以在需要时调用它来手动清理事件监听器。
默认在绑定的 target 对象更新时,会先调用清理函数,再尝试绑定事件和如果当前 effectScope 销毁前会调用清理函数
const handleClick = (evt: Event) => {
console.log(evt)
}
const stop = useEventListener(document, "click", handleClick);
// 当不再需要监听事件时,调用清理函数
stop();
感谢阅读,敬请斧正!