当前位置: 首页 > Web前端 > HTML

【ahooks源码系列解读】(开篇)如何获取和监听DOM元素

时间:2023-03-29 11:48:20 HTML

前言由于工作中写了很多自定义Hook场景,在实现一个通用的场景功能时,可能没有想到已经有了实现了的Hook封装或者我什至不想从Hooks库中找,但是社区好的实现可以提高开发效率,降低bug率。公司项目中有一个依赖库ahooks,但是我用的次数不多,所以有打算深入学习ahooks,主要是为了更加熟练的提取和实现一些场景的Hooks,学习如何更好的自定义钩子。我打算开始阅读ahooks的源代码。学习ahooks源码的好处在我看来,学习ahooks常用Hooks封装有以下好处:熟悉如何根据需要提取对应的Hooks,封装通用逻辑,讲解源码实现思路,提炼核心实现,学习自定义Hooks通过学习源码最佳实践深入学习特定场景下的Hooks,在项目开发中通俗易懂,使用起来更得心应手关于源码系列本系列文章基于ahooksv3.7.4版本,以及关于ahooks源码解读陆续输出。按照ahooks官网的分类,我先从DOM章节说起。DOM章节包含的Hooks如下:useEventListener:优雅使用addEventListener。useClickAway:监听目标元素外的点击事件。useDocumentVisibility:addEventListener的优雅使用。useDrop&useDrag:处理元素拖动的钩子。useEventTarget:常用表单控件的OnChange和取值逻辑封装(通过e.target.value获取表单值),支持自定义值转换和重置功能。useExternal:动态注入JS或CSS资源,useExternal可以保证资源全局唯一。useTitle:用于设置页面标题。useFavicon:设置页面的favicon。useFullscreen:钩子来管理DOM全屏。useHover:监听DOM元素是否被鼠标悬停。useMutationObserver:一个Hook,用于监视指定DOM树的变化。useInViewport:观察元素是否在可见区域,以及元素的可见比例。useKeyPress:监听键盘按键,支持组合键,支持按键别名。useLongPress:监听目标元素的长按事件。useMouse:监听鼠标位置。useResponsive:获取响应信息。useScroll:监听元素的滚动位置。useSize:一个监听DOM节点大小变化的Hook。useFocusWithin:监听当前焦点是否在某个区域内,同css属性:focus-within。由于内容量大,DOM文章会分成几篇输出,这样每篇文章阅读起来不会太费时间,可以快速阅读。文章在解读源码的基础上,也会带出涉及到的JS基础知识。在学习源码的过程中,还可以查漏补缺。回到本文的正题,在查看DOM类下的Hooks时,发现使用较多的是getTargetElement方法和useEffectWithTarget内部的Hook,所以在说源码之前先了解一下这两个Hooks。DOM类Hooks使用规范中提到了如何获取DOM元素的三种类型的目标:ahooks大多数DOM类Hooks都会收到target参数,表示要处理的元素。target支持三种类型React.MutableRefObject,HTMLElement,()=>HTMLElement。React.MutableRefObjectexportdefault()=>{constref=useRef(null)constisHovering=useHover(ref)返回{isHovering?'hover':'leaveHover'}

}HTMLElementexportdefault()=>{constisHovering=useHover(document.getElementById('test'))return{isHovering?'hover':'leaveHover'}
}支持()=>HTMLElement,一般适用于SSR场景exportdefault()=>{constisHovering=useHover(()=>document.getElementById('test'))返回{isHovering?'hover':'leaveHover'}
}getTargetElement为了兼容以上三种入参,ahooks封装了getTargetElement——获取目标DOM元素的方法。我们来看看代码做了什么:判断是否是浏览器环境,如果不是,则返回undefined判断目标元素是否为空,如果为空,则返回函数参数指定的默认元素核心:如果是是一个函数,函数执行后返回结果如果有current属性,则返回.current属性的值,兼容React.MutableRefObject类型,以上都不是,代表一个普通的DOM元素,并且直接返回导出函数getTargetElement(target:BasicTarget,defaultElement?:T){//判断是否是浏览器环境if(!isBrowser){returnundefined;}//如果目标元素为空,返回函数参数指定的默认元素if(!target){returndefaultElement;}让targetElement:TargetValue;//支持函数执行returnif(isFunction(target)){targetElement=target();}elseif('current'intarget){//兼容React.MutableRefObject类型,返回.current属性的值targetElement=target.current;}else{//普通DOM元素targetElement=target;}returntargetElement;}对应的TS类型:typeTargetValue=T|未定义|空类型TargetType=HTMLElement|元素|窗口|文档导出类型BasicTarget=|(()=>TargetValue)|目标值|MutableRefObject>监听DOM元素target,支持动态改变ahooks的DOM类Hooks。usagespecification的第二点指出:DOM类Hooks的target支持动态变化,如下:ref2=useRef(null)constisHovering=useHover(boolean?ref:ref2)return(<>{isHovering?'hover':'leaveHover'}
{isHovering?'hover':'leaveHover'})}useEffectWithTarget为了满足上面的条件,在useEffectWithTarget(packages/hooks/src/utils/useEffectWithTarget.ts)里面封装了ahooks,看这个文件的代码:import{useEffect}from'react'importcreateEffectWithTargetfrom'./createEffectWithTarget'constuseEffectWithTarget=createEffectWithTarget(useEffectWithTarget)exportffectWithTarget看到其实用的是createEffectWithTarget方法,传入的参数是useEffect(packages/hooks/src/utils/createEffectWithTarget.ts)createEffectWithTarget接受参数使用效果或使用布局Effect,返回useEffectWithTarget函数useEffectWithTarget函数接收三个参数:前两个参数为effect和deps(与useEffect参数一致),第三个参数兼容三种DOM元素,可以通过普通DOM/reftypes/functionstypeuseEffectWithTarget的实现思路:使用useEffect/useLayoutEffect进行监听,内部不传递第二个参数依赖,每次更新都会执行副作用函数。使用hasInitRef判断是否是第一次执行,如果是,则初始化:记录最后一次的目标元素列表和Dependency,执行效果函数由于每次更新都会执行useEffectType函数体,所以最新的targets和deps会每次都获取,所以后面的执行可以和第2点记录的最后一个ref值进行比较,不是第一次执行:如果元素列表的长度或者目标元素或者依赖发生变化,执行更新过程:执行上次返回的unload函数,更新最新值,重新执行effect组件的unload:执行unLoadRef.current?.()unload函数,resethasInitRefconstcreateEffectWithTarget=(useEffectType:typeofuseEffect|typeofuseLayoutEffect,)=>{/****@parameffect*@paramdeps*@paramtargettarget应该比较ref.current与ref.current,dom与dom,()=>dom与()=>dom*/constuseEffectWithTarget=(效果:EffectCallback,deps:DependencyList,目标:BasicTarget|BasicTarget[],)=>{//判断consthasInitRef是否已经初始化=useRef(false)constlastElementRef=useRef<(Element|null)[]>([])//lastconstlastDepsRef=useRef([])constunLoadRef=useRef()//useEffectType:代表useEffect或useLayoutEffect,每个Everyupdate会执行这个函数useEffectType(()=>{consttargets=Array.isArray(target)?target:[target]constels=targets.map((item)=>getTargetElement(item))//获取DOM元素列表//第一次执行:初始化if(!hasInitRef.current){hasInitRef.current=truelastElementRef.current=els//对应目标元素最后执行lastDepsRef.current=deps//对应依赖unLoadRef最后执行.current=effect()//执行从外部传入的效果函数,返回卸载函数return}//不是第一次执行:判断元素列表长度或目标元素或依赖变化if(els.length!==lastElementRef.current.length||!depsAreSame(els,lastElementRef.current)||!depsAreSame(deps,lastDepsRef.current)){//依赖发生了变化,相当于使用useEffect更新过程unLoadRef.current?.()lastElmentRef.current=elslastDepsRef.current=depsunLoadRef.current=effect()//再次执行effect,将unload函数赋值给unLoadRef}})//如果不传第二个参数,每次都会执行//卸载操作HookuseUnmount(()=>{unLoadRef.current?.()//为react-refresh执行卸载操作//hasInitRef.current=false})}returnuseEffectWithTarget}depsAreSameImplementation:importtype{DependencyList}from'react'exportdefaultfunctiondepsAreSame(oldDeps:DependencyList,deps:DependencyList,):boolean{if(oldDeps===deps)returntrue//浅比较for(leti=0;i