前言 React的开发离不开hooks,在社区中也有各种的hooks工具库。本篇阅读的就是react-use 工具库
希望通过阅读源码,加深自己对于React Hook的理解。
useCustomCompareEffect React的useEffect
比较依赖项的规则是Object.is()
,它大部分情况下和===
的效果是相同的useCustomCompareEffect
Hook 用于当依赖项是对象或者数组时自定义依赖项比较方法,定义更加复杂的比较逻辑
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { useState } from "react" ;import useCustomCompareEffect from "./hooks/useCustomCompareEffect" ;function App ( ) { const [obj, setObj] = useState ({ name : 'kyrie' , age : 21 }) const depsEqual = (newDeps, prevDeps ) => { return (prevDeps[0 ]?.name === newDeps[0 ]?.name ) ? true : false } useCustomCompareEffect (() => { console .log ('🚀~~ useCustomCompareEffect working !' ); }, depsEqual, [obj]) return ( <div > <p > name: { obj.name }</p > <p > age: { obj.age }</p > <button onClick ={() => setObj({ ...obj, name: 'easy code sniper' })}>修改name</button > <button onClick ={() => setObj({ ...obj, age: 30 })}>修改age</button > </div > ) } export default App ;
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import { DependencyList , EffectCallback , useEffect, useRef } from 'react' ;const isPrimitive = (val: any ) => val !== Object (val);type DepsEqualFnType <TDeps extends DependencyList > = (prevDeps: TDeps, nextDeps: TDeps ) => boolean; const useCustomCompareEffect = <TDeps extends DependencyList >( effect: EffectCallback, deps: TDeps, depsEqual: DepsEqualFnType<TDeps> ) => { if (process.env .NODE_ENV !== 'production' ) { if (!(deps instanceof Array ) || !deps.length ) { console .warn ( '`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.' ); } if (deps.every (isPrimitive)) { console .warn ( '`useCustomCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' ); } if (typeof depsEqual !== 'function' ) { console .warn ( '`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list' ); } } const ref = useRef<TDeps | undefined >(undefined ); if (!ref.current || !depsEqual (deps, ref.current )) { ref.current = deps; } useEffect (effect, ref.current ); }; export default useCustomCompareEffect;
useShallowCompareEffect React的useEffect
比较依赖项的规则是Object.is()
,它大部分情况下和===
的效果是相同的useShallowCompareEffect
Hook 会对每个依赖项进行浅比较,而不是引用相等
浅比较 通常是检查对象的顶层属性是否具有相同的值和相同的引用,对于嵌套对象不进行深入比较
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { DependencyList , EffectCallback } from 'react' ;import { equal as isShallowEqual } from 'fast-shallow-equal' ;import useCustomCompareEffect from './useCustomCompareEffect' ;const isPrimitive = (val: any ) => val !== Object (val);const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList ) => prevDeps.every ((dep, index ) => isShallowEqual (dep, nextDeps[index])); const useShallowCompareEffect = (effect: EffectCallback, deps: DependencyList ) => { if (process.env .NODE_ENV !== 'production' ) { if (!(deps instanceof Array ) || !deps.length ) { console .warn ( '`useShallowCompareEffect` should not be used with no dependencies. Use React.useEffect instead.' ); } if (deps.every (isPrimitive)) { console .warn ( '`useShallowCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.' ); } } useCustomCompareEffect (effect, deps, shallowEqualDepsList); }; export default useShallowCompareEffect;
useEvent 用于在特定的事件目标(如 window、document 或任何实现了特定事件监听器接口的对象)上添加和移除事件监听器,它旨在 在不同类型的事件目标上 提供一个统一的 API
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import { useEffect } from 'react' ;import { isBrowser, off, on } from './misc/util' ;export interface ListenerType1 { addEventListener (name : string, handler : (event?: any ) => void , ...args : any[]); removeEventListener (name : string, handler : (event?: any ) => void , ...args : any[]); } export interface ListenerType2 { on (name : string, handler : (event?: any ) => void , ...args : any[]); off (name : string, handler : (event?: any ) => void , ...args : any[]); } export type UseEventTarget = ListenerType1 | ListenerType2 ;const isListenerType1 = (target : any): target is ListenerType1 => { return !!target.addEventListener ; }; const isListenerType2 = (target : any): target is ListenerType2 => { return !!target.on ; }; const defaultTarget = isBrowser ? window : null ;const useEvent = ( name, handler, target, options? ) => { useEffect (() => { if (!handler || !target) { return ; } if (isListenerType1 (target)) { on (target, name, handler, options); } else if (isListenerType2 (target)) { target.on (name, handler, options); } return () => { if (isListenerType1 (target)) { off (target, name, handler, options); } else if (isListenerType2 (target)) { target.off (name, handler, options); } }; }, [name, handler, target, JSON .stringify (options)]); }; export default useEvent;
on
和 off
函数的封装:
on函数接受一个可能为null的对象obj和一个参数列表args。参数列表可以是任何addEventListener方法接受的参数。函数内部会检查obj是否存在并且是否有addEventListener方法。如果条件满足,它会调用obj.addEventListener并传入args参数。 这个函数的主要作用是检查对象是否存在并且可以添加事件监听器,然后按照给定的参数调用addEventListener方法。这样做可以避免直接在组件内部调用addEventListener时可能遇到的null或undefined对象错误。
off函数同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function on<T extends Window | Document | HTMLElement | EventTarget >( obj : T | null , ...args : Parameters <T['addEventListener' ]> | [string, Function | null , ...any] ): void { if (obj && obj.addEventListener ) { obj.addEventListener (...(args as Parameters <HTMLElement ['addEventListener' ]>)); } } export function off<T extends Window | Document | HTMLElement | EventTarget >( obj : T | null , ...args : Parameters <T['removeEventListener' ]> | [string, Function | null , ...any] ): void { if (obj && obj.removeEventListener ) { obj.removeEventListener (...(args as Parameters <HTMLElement ['removeEventListener' ]>)); } }
useGeolocation 该hook用于获取和追踪用户的地理位置,基于浏览器内置对象navigator
实现
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import React from "react" ;import useGeolocation from "./hooks/useGeolocation" ;function App ( ) { let geolocation = useGeolocation () console .log (geolocation); return ( <div className ="App" > </div > ); } export default App ;
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 import { useEffect, useState } from 'react' ;export interface IGeolocationPositionError { readonly code : number; readonly message : string; readonly PERMISSION_DENIED : number; readonly POSITION_UNAVAILABLE : number; readonly TIMEOUT : number; } export interface GeoLocationSensorState { loading : boolean; accuracy : number | null ; altitude : number | null ; altitudeAccuracy : number | null ; heading : number | null ; latitude : number | null ; longitude : number | null ; speed : number | null ; timestamp : number | null ; error?: Error | IGeolocationPositionError ; } export interface PositionOptions { enableHighAccuracy?: boolean; maximumAge?: number; timeout?: number; } const useGeolocation = (options?: PositionOptions ): GeoLocationSensorState => { const [state, setState] = useState<GeoLocationSensorState >({ loading : true , accuracy : null , altitude : null , altitudeAccuracy : null , heading : null , latitude : null , longitude : null , speed : null , timestamp : Date .now (), }); let mounted = true ; let watchId : any; const onEvent = (event: any ) => { if (mounted) { setState ({ loading : false , accuracy : event.coords .accuracy , altitude : event.coords .altitude , altitudeAccuracy : event.coords .altitudeAccuracy , heading : event.coords .heading , latitude : event.coords .latitude , longitude : event.coords .longitude , speed : event.coords .speed , timestamp : event.timestamp , }); } }; const onEventError = (error: IGeolocationPositionError ) => mounted && setState ((oldState ) => ({ ...oldState, loading : false , error })); useEffect (() => { navigator.geolocation .getCurrentPosition (onEvent, onEventError, options); watchId = navigator.geolocation .watchPosition (onEvent, onEventError, options); return () => { mounted = false ; navigator.geolocation .clearWatch (watchId); }; }, []); return state; }; export default useGeolocation;
navigator对象 是 浏览器内置对象,它提供了关于用户浏览器的信息
useHover useHover
提供一种简单的方式来跟踪鼠标悬停状态。这个钩子接受一个 React 元素或者一个返回 React 元素的函数,并返回一个数组,包含一个带有悬停事件处理的克隆元素以及一个表示悬停状态(true 或 false)的布尔值。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import React from "react" ;import useHover from "./hooks/useHover" ;function App ( ) { const [ el, state ] = useHover ( <div onMouseEnter ={() => console.log('🚀~~ enter')} onMouseLeave={() => console.log('🚀~~ leave')} >我是一个div</div > ) return ( <div className ="App" > {el} {state && <p > Hovered !</p > } </div > ); } export default App ;
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import * as React from 'react' ;import { noop } from './misc/util' ;const { useState } = React ;export type Element = ((state: boolean ) => React .ReactElement <any>) | React .ReactElement <any>;const useHover = (element : Element ): [React .ReactElement <any>, boolean] => { const [state, setState] = useState (false ); const onMouseEnter = (originalOnMouseEnter?: any ) => (event: any ) => { (originalOnMouseEnter || noop)(event); setState (true ); }; const onMouseLeave = (originalOnMouseLeave?: any ) => (event: any ) => { (originalOnMouseLeave || noop)(event); setState (false ); }; if (typeof element === 'function' ) { element = element (state); } const el = React .cloneElement (element, { onMouseEnter : onMouseEnter (element.props .onMouseEnter ), onMouseLeave : onMouseLeave (element.props .onMouseLeave ), }); return [el, state]; }; export default useHover;
React.cloneElement()
它允许你克隆一个React元素,并且可以选择性的传入props、子元素以及key。它的作用是基于已有的React元素创建一个新的元素,默认保留原有元素的props、内部状态和行为
默认保留原有元素的props、内部状态和行为 因为在默认情况下,React只是浅克隆了原有元素,并传入新的props。在不改变key的情况下,React会视这两个组件为同一个实例,这就意味着,即使属性可能有所改变,但由于组件类型和 key 没有变化,React的重用逻辑会保留组件实例及其内部状态。
使用React.cloneElement
可以很方便地扩展或修改组件的子元素,而不需要创建一个全新的组件。
1 2 3 4 5 React .cloneElement ( element, props, [...children] )
React.cloneElement
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React from 'react' ;const ListItem = ({ children, onClick } ) => { return <li onClick ={onClick} > {children}</li > ; }; const List = ({ items } ) => { return ( <ul > {items.map((item, index) => { // 克隆ListItem,并为每个item添加特定的onClick处理器 return React.cloneElement(<ListItem > {item.text}</ListItem > , { key: index, // 必须提供key,特别是在map循环中 onClick: () => alert(`Item ${index + 1} clicked!`), // 绑定了一个弹窗事件 }); })} </ul > ); }; const App = ( ) => { const items = [ { text : 'Item 1' }, { text : 'Item 2' }, { text : 'Item 3' }, ]; return <List items ={items} /> ; }; export default App ;
useHoverDirty useHoverDirty
提供一种方式来检测用户是否悬浮在某个元素上
useHover
接受一个React元素或者一个函数,而useHoverDirty
接受React ref
useHover
通过设置React元素的onMouseOver
和onMouseOut
事件,而useHoverDirty
通过设置DOM元素的onMouseOver
和onMouseOut
事件
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import React , { useRef } from 'react' ;import useHoverDirty from './useHoverDirty' ;const HoverComponent = ( ) => { const hoverRef = useRef (null ); const isHovered = useHoverDirty (hoverRef); const style = { width : '200px' , height : '200px' , backgroundColor : isHovered ? 'blue' : 'red' , color : 'white' , display : 'flex' , alignItems : 'center' , justifyContent : 'center' , transition : 'background-color 0.3s' }; return ( <div ref ={hoverRef} style ={style} > {isHovered ? 'Hovering' : 'Not Hovering'} </div > ); }; export default HoverComponent ;
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import { RefObject , useEffect, useState } from 'react' ;import { off, on } from './misc/util' ;const useHoverDirty = (ref: RefObject<Element>, enabled: boolean = true ) => { if (process.env .NODE_ENV === 'development' ) { if (typeof ref !== 'object' || typeof ref.current === 'undefined' ) { console .error ('useHoverDirty expects a single ref argument.' ); } } const [value, setValue] = useState (false ); useEffect (() => { const onMouseOver = ( ) => setValue (true ); const onMouseOut = ( ) => setValue (false ); if (enabled && ref && ref.current ) { on (ref.current , 'mouseover' , onMouseOver); on (ref.current , 'mouseout' , onMouseOut); } const { current } = ref; return () => { if (enabled && current) { off (current, 'mouseover' , onMouseOver); off (current, 'mouseout' , onMouseOut); } }; }, [enabled, ref]); return value; }; export default useHoverDirty;export function on<T extends Window | Document | HTMLElement | EventTarget >( obj : T | null , ...args : Parameters <T['addEventListener' ]> | [string, Function | null , ...any] ): void { if (obj && obj.addEventListener ) { obj.addEventListener (...(args as Parameters <HTMLElement ['addEventListener' ]>)); } } export function off<T extends Window | Document | HTMLElement | EventTarget >( obj : T | null , ...args : Parameters <T['removeEventListener' ]> | [string, Function | null , ...any] ): void { if (obj && obj.removeEventListener ) { obj.removeEventListener (...(args as Parameters <HTMLElement ['removeEventListener' ]>)); } }
useIntersection useIntersection
使用了浏览器的 Intersection Observer API 来异步地检测目标元素与其祖先元素或顶级文档视口的交叉状态的变化。
Intersection Observer API 是一个浏览器API,它允许你异步观察一个元素与其祖先元素或全局视口的交集变化。能够高效得获取元素是否进入或离开视口的信息。
相关概念
root(根)
: 想要检查目标元素与之相交的容器元素,如果未指定或为null,默认为浏览器视口
rootMargin(根边界)
: 类似于CSS的margin属性。它允许你指定一个在根元素的外围形成的边框区域,用于增加或减少用于检查交集的区域大小
threshold(阈值)
: 一个数字或由多个数字组成的数组,指定了观察者的回调函数被执行的条件。例如,如果阈值是0.5,则目标元素有50%进入根元素时,观察者的回调函数将被执行
IntersectionObserver()
方法 是一个构造函数,用于创建一个新的 IntersectionObserver 对象。 这个构造函数接受两个参数:一个回调函数和一个可选的配置对象。
Intersection Observer
root
: 想要检查目标元素与之相交的容器元素,如果未指定或为null,默认为浏览器视口
rootMargin
: 类似于CSS的margin属性。它允许你指定一个在根元素的外围形成的边框区域,用于增加或减少用于检查交集的区域大小
thresholds
: 一个数字或由多个数字组成的数组,指定了观察者的回调函数被执行的条件。例如,如果阈值是0.5,则目标元素有50%进入根元素时,观察者的回调函数将被执行
disconnect()
: 停止观察所有目标
observe(target)
: 开始观察一个目标元素
takeRecords
: 返回所有目标的IntersectionObserverEntry
对象数组
unobserve(targrt)
: 停止观察一个目标元素
Intersection Observer Entry
boundingClientRect
: 返回目标元素的矩形信息,用于计算与视口的交集
intersectionRatio
: 目标元素可见部分 占总体的比例
intersectionRect
: 目标元素与视口交叉部分 的矩形信息
isIntersecting
: 目标元素是否与根元素相交
rootBounds
: 根元素的矩形信息
target
: 目标元素
time
: 观察到交叉状态变化时的时间戳
使用场景
懒加载:当图片或其他资源进入视口时才加载它们,以此节省带宽和提高页面加载速度
无限滚动: 当用户滚动到页面底部时,自动加载更多内容
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { RefObject , useEffect, useState } from 'react' ;const useIntersection = ( ref : RefObject <HTMLElement >, options : IntersectionObserverInit ): IntersectionObserverEntry | null => { const [intersectionObserverEntry, setIntersectionObserverEntry] = useState<IntersectionObserverEntry | null >(null ); useEffect (() => { if (ref.current && typeof IntersectionObserver === 'function' ) { const handler = (entries: IntersectionObserverEntry[] ) => { setIntersectionObserverEntry (entries[0 ]); }; const observer = new IntersectionObserver (handler, options); observer.observe (ref.current ); return () => { setIntersectionObserverEntry (null ); observer.disconnect (); }; } return () => {}; }, [ref.current , options.threshold , options.root , options.rootMargin ]); return intersectionObserverEntry; }; export default useIntersection;
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import React , { useRef } from "react" ;import useIntersection from "./hooks/useIntersection" ;function App ( ) { const intersectionRef = useRef (null ) const intersection = useIntersection (intersectionRef, { root : null , rootMargin : '0px' , threshold : 1 }) console .log (intersection); return ( <div style ={{ width: '200px ', height: '200px ', backgroundColor: 'yellow ', overflowY: 'scroll ' }}> <div style ={{ width: '100 %', height: '200px ' }} > </div > <div style ={{ width: '100px ', height: '50px ', backgroundColor: 'skyblue ' }} ref ={intersectionRef} > { intersection && (intersection.intersectionRatio < 1) ? 'Obscured' : 'Full in view' } </div > </div > ) } export default App ;
useKey useKey
用于设置监听键盘事件
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 import {useKey} from 'react-use' ;const Demo = ( ) => { const [count, set] = useState (0 ); const increment = ( ) => set (count => ++count); useKey ('ArrowUp' , increment); return ( <div > Press arrow up: {count} </div > ); };
源码及解析如下;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 import { DependencyList , useMemo } from 'react' ;import useEvent, { UseEventOptions , UseEventTarget } from './useEvent' ;import { noop } from './misc/util' ;export type KeyPredicate = (event: KeyboardEvent ) => boolean;export type KeyFilter = null | undefined | string | ((event: KeyboardEvent ) => boolean);export type Handler = (event: KeyboardEvent ) => void ;export interface UseKeyOptions <T extends UseEventTarget > { event?: 'keydown' | 'keypress' | 'keyup' ; target?: T | null ; options?: UseEventOptions <T>; } const createKeyPredicate = (keyFilter : KeyFilter ): KeyPredicate => typeof keyFilter === 'function' ? keyFilter : typeof keyFilter === 'string' ? (event: KeyboardEvent ) => event.key === keyFilter : keyFilter ? () => true : () => false ; const useKey = <T extends UseEventTarget >( key: KeyFilter, fn: Handler = noop, opts: UseKeyOptions<T> = {}, deps: DependencyList = [key] ) => { const { event = 'keydown' , target, options } = opts; const useMemoHandler = useMemo (() => { const predicate : KeyPredicate = createKeyPredicate (key); const handler : Handler = (handlerEvent ) => { if (predicate (handlerEvent)) { return fn (handlerEvent); } }; return handler; }, deps); useEvent (event, useMemoHandler, target, options); }; export default useKey;
useLongPress useLongPress
用于设置长按之后触发的回调
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 import { useCallback, useRef } from 'react' ;import { off, on } from './misc/util' ;interface Options { isPreventDefault?: boolean; delay?: number; } const isTouchEvent = (ev : Event ): ev is TouchEvent => { return 'touches' in ev; }; const preventDefault = (ev: Event ) => { if (!isTouchEvent (ev)) return ; if (ev.touches .length < 2 && ev.preventDefault ) { ev.preventDefault (); } }; const useLongPress = ( callback: (e: TouchEvent | MouseEvent) => void , { isPreventDefault = true , delay = 300 }: Options = {} ) => { const timeout = useRef<ReturnType <typeof setTimeout >>(); const target = useRef<EventTarget >(); const start = useCallback ( (event: TouchEvent | MouseEvent ) => { if (isPreventDefault && event.target ) { on (event.target , 'touchend' , preventDefault, { passive : false }); target.current = event.target ; } timeout.current = setTimeout (() => callback (event), delay); }, [callback, delay, isPreventDefault] ); const clear = useCallback (() => { timeout.current && clearTimeout (timeout.current ); if (isPreventDefault && target.current ) { off (target.current , 'touchend' , preventDefault); } }, [isPreventDefault]); return { onMouseDown : (e: any ) => start (e), onTouchStart : (e: any ) => start (e), onMouseUp : clear, onMouseLeave : clear, onTouchEnd : clear, } as const ; }; export default useLongPress;
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { useLongPress } from './useLongPress' ;const Demo = ( ) => { const onLongPress = ( ) => { console .log ('calls callback after long pressing 300ms' ); }; const defaultOptions = { isPreventDefault : true , delay : 300 , }; const longPressEvent = useLongPress (onLongPress, defaultOptions); return <button {...longPressEvent }> useLongPress</button > ; };
useMeasure useMeasure
用于测量一个DOM元素的尺寸和位置,并在尺寸或位置变化时更新这些信息。这个Hook依赖于ResizeObserver API
,它允许你监听一个元素的大小变化。
ResizeObserver
是一个强大的Web API,允许开发者监听HTML元素的尺寸变化。
基本用法
使用ResizeObserver
创建一个observer实例,并给它提供一个回调函数,该函数会在被观察元素的尺寸位置发生变化时被调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const resizeObserver = new ResizeObserver (entries => { for (let entry of entries) { const { width, height } = entry.contentRect ; console .log ('Element:' , entry.target ); console .log (`Element size: ${width} px x ${height} px` ); } }); const myElement = document .getElementById ('myElement' );resizeObserver.observe (myElement); resizeObserver.unobserve (myElement); resizeObserver.disconnect ();
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import { useMemo, useState, useEffect } from 'react' ;import { isBrowser, noop } from './misc/util' ;export type UseMeasureRect = Pick < DOMRectReadOnly , 'x' | 'y' | 'top' | 'left' | 'right' | 'bottom' | 'height' | 'width' >; export type UseMeasureRef <E extends Element = Element > = (element: E ) => void ;export type UseMeasureResult <E extends Element = Element > = [UseMeasureRef <E>, UseMeasureRect ];const defaultState : UseMeasureRect = { x : 0 , y : 0 , width : 0 , height : 0 , top : 0 , left : 0 , bottom : 0 , right : 0 , }; function useMeasure<E extends Element = Element >(): UseMeasureResult <E> { const [element, ref] = useState<E | null >(null ); const [rect, setRect] = useState<UseMeasureRect >(defaultState); const observer = useMemo ( () => new (window as any).ResizeObserver ((entries ) => { if (entries[0 ]) { const { x, y, width, height, top, left, bottom, right } = entries[0 ].contentRect ; setRect ({ x, y, width, height, top, left, bottom, right }); } }), [] ); useEffect (() => { if (!element) return ; observer.observe (element); return () => { observer.disconnect (); }; }, [element]); return [ref, rect]; } export default isBrowser && typeof (window as any).ResizeObserver !== 'undefined' ? useMeasure : ((() => [noop, defaultState]) as typeof useMeasure);
useMouse useMouse
用于动态跟踪鼠标的位置
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import { RefObject , useEffect } from 'react' ;import useRafState from './useRafState' ;import { off, on } from './misc/util' ;export interface State { docX : number; docY : number; posX : number; posY : number; elX : number; elY : number; elH : number; elW : number; } const useMouse = (ref : RefObject <Element >): State => { const [state, setState] = useRafState<State >({ docX : 0 , docY : 0 , posX : 0 , posY : 0 , elX : 0 , elY : 0 , elH : 0 , elW : 0 , }); useEffect (() => { const moveHandler = (event: MouseEvent ) => { if (ref && ref.current ) { const { left, top, width : elW, height : elH } = ref.current .getBoundingClientRect (); const posX = left + window .pageXOffset ; const posY = top + window .pageYOffset ; const elX = event.pageX - posX; const elY = event.pageY - posY; setState ({ docX : event.pageX , docY : event.pageY , posX, posY, elX, elY, elH, elW, }); } }; on (document , 'mousemove' , moveHandler); return () => { off (document , 'mousemove' , moveHandler); }; }, [ref]); return state; }; export default useMouse;
usePageLeave usePageLeave
用于当鼠标离开页面时触发一个回调
event.relatedTarget
和 event.toElement
浏览器默认事件对象中的属性,relatedTarget
属性是一个事件属性,它在某些特定的事件中提供了关于事件的额外上下文。这个属性特别用于鼠标事件,比如mouseover和mouseout,以及焦点事件,比如focusin和focusout。
relatedTarget属性引用了与事件相关的一个DOM元素
对于鼠标事件 :
在mouseover事件中,relatedTarget
属性引用的是鼠标刚刚离开的那个元素,即鼠标指针之前所在的元素。
在mouseout事件中,relatedTarget
属性引用的是鼠标即将移动到的那个元素,即鼠标指针即将进入的元素。
对于焦点事件 :
在focusin(或focus)事件中,relatedTarget
属性引用的是失去焦点的元素,即焦点从哪个元素移开。 在focusout(或blur)事件中,relatedTarget
属性引用的是即将获得焦点的元素,即焦点将要移到哪个元素上。
toElement
的作用与relatedTarget
相似,特别是在mouseover和mouseout事件上,用于兼容旧版本浏览器
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { useEffect } from 'react' ;import { off, on } from './misc/util' ;const usePageLeave = (onPageLeave, args = [] ) => { useEffect (() => { if (!onPageLeave) { return ; } const handler = (event ) => { event = event ? event : (window .event as any); const from = event.relatedTarget || event.toElement ; if (!from || (from as any).nodeName === 'HTML' ) { onPageLeave (); } }; on (document , 'mouseout' , handler); return () => { off (document , 'mouseout' , handler); }; }, args); }; export default usePageLeave;
useClickAway useClickAway
用于当用户在目标元素外部单击时触发回调
源码及解析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { RefObject , useEffect, useRef } from 'react' ;import { off, on } from './misc/util' ;const defaultEvents = ['mousedown' , 'touchstart' ];const useClickAway = <E extends Event = Event >( ref: RefObject<HTMLElement | null >, onClickAway: (event: E) => void , events: string[] = defaultEvents ) => { const savedCallback = useRef (onClickAway); useEffect (() => { savedCallback.current = onClickAway; }, [onClickAway]); useEffect (() => { const handler = (event ) => { const { current : el } = ref; el && !el.contains (event.target ) && savedCallback.current (event); }; for (const eventName of events) { on (document , eventName, handler); } return () => { for (const eventName of events) { off (document , eventName, handler); } }; }, [events, ref]); }; export default useClickAway;