如何封装一个hooks解决ClickAway
最近在学习ahooks的源码,打算写些文章记录并分享,讲解ahooks中的一些方法是如何实现的,并实现一个mini版本。如果你对该文章或者系列有任何建议,欢迎打出来我们一起探讨。
前言
我们日常开发中,有很多时候需要监听用户是否点击到了某个元素外面。
useClickAway
可以帮助我们在点击元素外部时触发回调函数,比如点击Modal/Dialog/Popover 外部就关闭弹窗等。
在本篇文章中,我们将通过注释逐行讲解 ahooks 中的 useClickAway 方法的源码。
源码解读
useClickAway 的原理也并不复杂,其实就是监听整个文档的点击事件,判断点击的元素是否在目标元素内部,如果不在则执行回调函数。
那么如何把上述行为封装到一个hooks里的呢?
我们来看一下官方示例中的基础用法
import React, { useState, useRef } from 'react';
import { useClickAway } from 'ahooks';
export default () => {
const [counter, setCounter] = useState(0);
const ref = useRef<HTMLButtonElement>(null);
useClickAway(() => {
setCounter((s) => s + 1);
}, ref);
return (
<div>
<button ref={ref} type="button">
box
</button>
<p>counter: {counter}</p>
</div>
);
};
那么我们已经可以大致推测 useClickAway
是如何实现的
- 监听整个文档的点击事件:通过传入的
ref
,useClickAway
可以获取到目标元素的引用。然后在useEffect
监听document的点击事件。 - 判断点击的元素是否在目标元素内部:当点击事件触发时,判断点击的元素是否在目标元素内部。如果不在,则执行传入的回调函数。一般情况下我们会利用
element.contains
进行判断。
至于具体是怎么实现的,我们来看下真正的源码。
下面,是整体源码的解读,如果你想看简单的实现,也可以直接跳到动手实现的部分:
import useLatest from '../useLatest';
import type { BasicTarget } from '../utils/domTarget';
import { getTargetElement } from '../utils/domTarget'; // 获取目标元素的实际 DOM 元素
import getDocumentOrShadow from '../utils/getDocumentOrShadow';
import useEffectWithTarget from '../utils/useEffectWithTarget';
type DocumentEventKey = keyof DocumentEventMap;
export default function useClickAway<T extends Event = Event>(
onClickAway: (event: T) => void, // 点击外部区域时触发的回调函数
target: BasicTarget | BasicTarget[], // 可以指定一个或多个元素作为点击对象
eventName: DocumentEventKey | DocumentEventKey[] = 'click', // 要监听的事件类型,默认为 'click'
) {
// 使用 useLatest 创建一个持续更新的引用
const onClickAwayRef = useLatest(onClickAway);
// 使用 useEffectWithTarget 处理事件监听的添加和移除
useEffectWithTarget(
() => {
// 创建事件处理函数
const handler = (event: any) => {
// 确保target是个数组类型
const targets = Array.isArray(target) ? target : [target];
// 检查点击事件的目标是否在指定的 target 内部
if (
targets.some((item) => {
const targetElement = getTargetElement(item);
return !targetElement || targetElement.contains(event.target);
})
) {
return;
}
// 不在指定的 target 内部,则触发 onClickAway 回调
onClickAwayRef.current(event);
};
// 获取正确的 document 或 shadowRoot 对象
const documentOrShadow = getDocumentOrShadow(target);
// 将 eventName 也给转为数组形式
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
// 遍历事件名称数组,添加事件监听器
eventNames.forEach((event) => documentOrShadow.addEventListener(event, handler));
// 组件卸载时移除事件监听器
return () => {
eventNames.forEach((event) => documentOrShadow.removeEventListener(event, handler));
};
},
// 依赖项数组,当 eventName 或 target 发生变化时重新执行 Effect Hook
Array.isArray(eventName) ? eventName : [eventName],
target,
);
}
我们可以看到实现的方式和我们之前说过的也很接近,但ahooks做了更多的边界的考虑,比如你可以传入一个元素数组作为targets,再比如除了document之外他们也还考虑到了shadowRoot的情况,甚至你也可以自定义dom事件类型,并不是非要用 click
事件!
至于useLatest,你可以通过下面的场景来理解:
当一个组件需要在用户输入时进行一些操作,但又不想在每次输入时重新渲染整个组件时,就可以使用useLatest来保存input时的回调函数。这样,在回调函数中既可以访问到最新的状态和属性值,又不需要重新渲染整个组件。
当然,它也并不影响你理解整个useClickAway的流程。以后我们也会出文章讲解下useLatest,其实也很简单,源码不到10行。
动手实现
在大概了解了 useClickAway
的源码之后,我们也可以尝试着自己不引入任何依赖,手动去实现一个mini版本。
整理一下思路和先前说过的一致:
- 监听整个文档的点击事件,并在点击时判断点击的元素是否在目标元素内部
- 如果不在目标元素内部,则执行传入的回调函数
实现的mini版本大概如下所示:
import { RefObject, useEffect } from 'react';
export function useClickAway(onClickAway: (event: MouseEvent) => void, targetRef: RefObject<HTMLElement | null>) {
useEffect(() => {
const handler = (event: MouseEvent) => {
if (!targetRef.current || targetRef.current.contains(event.target as Node)) {
return;
}
onClickAway(event);
};
document.addEventListener('click', handler);
return () => {
document.removeEventListener('click', handler);
};
}, [onClickAway, targetRef]);
}
我们不需要去考虑数组,也不需要去考虑ShadowRoot,也不需要考虑别的。这就是最简单的实现啦。
效果预览:
import { useClickAway } from '@/hooks/useClickAway'
import { useRef, useState } from 'react'
const ClickAway = () => {
const ref = useRef(null)
const [count, setCount] = useState(0)
useClickAway(() => {
setCount(count + 1)
}, ref)
return (
<div>
<div ref={ref} className="p-20px bg-gray-200">
Click outside count: {count}
</div>
</div>
)
}
export default ClickAway
总结
通过分析源码和示例,我们了解了 useClickAway 的工作原理,以及如何利用它来处理点击元素外部的事件。
在源码解读部分,我们逐步分析了 useClickAway 方法的实现细节,包括监听整个文档的点击事件、判断点击的元素是否在目标元素内部等。此外,我们还尝试手动实现了一个简化版的 useClickAway 功能,通过自定义 Hook 来实现相似的功能。
点击外部关闭弹窗这种需求在实际开发中经常会遇到,通过 useClickAway 这个自定义 Hook,我们可以很方便地实现这样的功能,提高用户体验。
如果这篇文章对你有帮助的话,还请你不吝小手点一个免费的赞,这会给我很大的鼓励,谢谢你!
转载自:https://juejin.cn/post/7341759161337954339