easycodesniper

blog by chen qiyi

react-use源码阅读

前言

React的开发离不开hooks,在社区中也有各种的hooks工具库。本篇阅读的就是react-use工具库

希望通过阅读源码,加深自己对于React Hook的理解。

useCustomCompareEffect

React的useEffect比较依赖项的规则是Object.is(),它大部分情况下和===的效果是相同的
useCustomCompareEffectHook 用于当依赖项是对象或者数组时自定义依赖项比较方法,定义更加复杂的比较逻辑

使用示例:

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
})
// 自定义比较函数,只有当name值发生变化是重新渲染
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';

// 判断是否是基本类型值,是基本类型就返回true
// Object():给Object()函数传递一个值,如果是对象或者数组,则返回该值的引用;如果是基本类型值,则把该基本类型值包装成为对应的对象类型
const isPrimitive = (val: any) => val !== Object(val);

// 自定义比较函数有两个参数:旧的deps 和 新的deps ,该函数需要返回一个bool值来表示依赖是否发生变化
type DepsEqualFnType<TDeps extends DependencyList> = (prevDeps: TDeps, nextDeps: TDeps) => boolean;

/**
* 接受三个参数
* effect:副作用函数
* deps:依赖项
* depsEqual:自定义比较函数,该函数有两个参数,前一个依赖项 和 当前依赖项,要求返回一个boolean来表示依赖项是否发生了变化
*/
const useCustomCompareEffect = <TDeps extends DependencyList>(
effect: EffectCallback,
deps: TDeps,
depsEqual: DepsEqualFnType<TDeps>
) => {
// 生产环境下的一些警告提示,可以忽略
if (process.env.NODE_ENV !== 'production') {
// deps依赖必须有
if (!(deps instanceof Array) || !deps.length) {
console.warn(
'`useCustomCompareEffect` should not be used with no dependencies. Use React.useEffect instead.'
);
}
// 如果每一项依赖都是基本类型,那使用useEffect即可
if (deps.every(isPrimitive)) {
console.warn(
'`useCustomCompareEffect` should not be used with dependencies that are all primitive values. Use React.useEffect instead.'
);
}
// depsEqual 必须是一个函数
if (typeof depsEqual !== 'function') {
console.warn(
'`useCustomCompareEffect` should be used with depsEqual callback for comparing deps list'
);
}
}
//定义一个ref来存储依赖项
const ref = useRef<TDeps | undefined>(undefined);

/*
* !ref.current 表示初次渲染
* !depsEqual(deps, ref.current) 根据自定义的比较函数来判断依赖是否发生变化
*/
if (!ref.current || !depsEqual(deps, ref.current)) {
ref.current = deps; // 当初次渲染时,或者依赖项发生变化时,ref存储新的依赖
}

// useEffect依赖的并不是deps,实际依赖的是ref.current
// 只有在depsEqual函数判断依赖项发生了变化,并更新ref.current之后,useEffect才会重新执行effect
useEffect(effect, ref.current);
};

export default useCustomCompareEffect;

useShallowCompareEffect

React的useEffect比较依赖项的规则是Object.is(),它大部分情况下和===的效果是相同的
useShallowCompareEffectHook 会对每个依赖项进行浅比较,而不是引用相等

浅比较
通常是检查对象的顶层属性是否具有相同的值和相同的引用,对于嵌套对象不进行深入比较

源码及解析如下:

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';

// 判断是否是基本类型值
// 给Object()函数传递一个值,如果是对象或者数组,则返回该值的引用;如果是基本类型值,则把该基本类型值包装成为对应的对象类型
const isPrimitive = (val: any) => val !== Object(val);

// 调用第三方库,实现依赖项的浅比较
const shallowEqualDepsList = (prevDeps: DependencyList, nextDeps: DependencyList) =>
prevDeps.every((dep, index) => isShallowEqual(dep, nextDeps[index]));

/**
* 接受两个参数
* effect:副作用函数
* deps:依赖项
*/
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 , 将shallowEqualDepsList作为自定义比较方法
// 实际上就是对 useCustomCompareEffect 的又一层封装
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';
// isBrower 判断是否是浏览器环境
// on off 是封装好的浏览器事件监听器添加和移除方法
import { isBrowser, off, on } from './misc/util';


// ListenerType1 和 ListenerType2 接口定义了两种不同的事件监听模式

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;

// isListenerType1 和 isListenerType2 用来判断目标事件对象是否有 addEventListener 或 on 属性来实现的

const isListenerType1 = (target: any): target is ListenerType1 => {
return !!target.addEventListener; // !! 任何真值都会被转化成true,假值都会被转化成false
};
const isListenerType2 = (target: any): target is ListenerType2 => {
return !!target.on;
};

const defaultTarget = isBrowser ? window : null;

/**
* 接受四个参数;
* name:事件名称
* handler:事件处理函数
* target:事件监听器的目标对象,默认值是 defaultTarget
* options:可选参数,用于传递给事件监听器的选项
*/
const useEvent = (
name,
handler,
target,
options?
) => {
useEffect(() => {
if (!handler || !target) { // 如果没有传入事件处理函数或者事件对象,不做任何处理直接返回
return;
}
if (isListenerType1(target)) { //如果目标对象实现的是addEventListener模式,将它手动代理成on模式来添加监听器
on(target, name, handler, options);
} else if (isListenerType2(target)) { //如果目标对象实现的是on模式,直接用on模式添加监听器
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;

onoff函数的封装:

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; // 调用navigator.geolocation.watchPosition返回的id,用于在组件卸载时清除监听

// 位置信息请求成功的回调函数,navigator.geolocation.getCurrentPosition会在成功时传给该函数一个Positon对象,包含了位置信息
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,
});
}
};

// 位置信息请求失败的回调函数,navigator.geolocation.getCurrentPosition会在失败时传给该函数一个error对象,包含了错误信息
const onEventError = (error: IGeolocationPositionError) =>
mounted && setState((oldState) => ({ ...oldState, loading: false, error }));


/**
* navigator.geolocation.getCurrentPosition用于获取当前地理位置信息
* 接受三个参数:成功的回调、错误时的回调(可选)、选项对象PositionOptions(可选)
*
* navigator.geolocation.watchPosition用于注册一个监听器,在用户的设备地理位置发生变化时被调用。该方法返回一个id,用于取消监听
* 接受三个参数:成功的回调、错误时的回调(可选)、选项对象PositionOptions(可选)
*
* navigator.geolocation.clearWatch(watchId)用于取消监听,传入监听器id
*/
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';

// noop 不执行任何操作的空函数 () => {}
import { noop } from './misc/util';

const { useState } = React;

//定义Element接口,表示Element类型可以是一个React元素,也可以是一个函数,该函数接受一个boolean类型的参数,并返回React元素
export type Element = ((state: boolean) => React.ReactElement<any>) | React.ReactElement<any>;

// useHover 接受一个Element类型的参数
const useHover = (element: Element): [React.ReactElement<any>, boolean] => {
// state状态用于表示是否处于鼠标悬停状态
const [state, setState] = useState(false);

// 接受一个原始事件处理函数,返回一个新的事件处理函数。
// 新的事件处理函数首先调用传入的原始事件处理函数(如果没有提供,则调用 noop),然后通过 setState 更新悬停状态。
const onMouseEnter = (originalOnMouseEnter?: any) => (event: any) => {
(originalOnMouseEnter || noop)(event);
setState(true);
};
const onMouseLeave = (originalOnMouseLeave?: any) => (event: any) => {
(originalOnMouseLeave || noop)(event);
setState(false);
};

// 传入的element是一个函数,则将代表是否hover的状态state传入来返回对应的React元素
if (typeof element === 'function') {
element = element(state);
}

// element.props.onMouseEnter 和 element.props.onMouseLeave 是原始元素上可能存在的鼠标悬停和离开函数
const el = React.cloneElement(element, {
onMouseEnter: onMouseEnter(element.props.onMouseEnter),
onMouseLeave: onMouseLeave(element.props.onMouseLeave),
});

// 返回克隆的React元素 以及 表示悬浮状态的state
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, // 想要克隆的React对象
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>
);
};

// 使用List组件
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元素的onMouseOveronMouseOut事件,而useHoverDirty通过设置DOM元素的onMouseOveronMouseOut事件

使用示例:

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 = () => {
// 创建一个ref来引用我们想要检测悬停的DOM元素
const hoverRef = useRef(null);
// 使用我们的自定义Hook来获取悬停状态
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';

/**
* ref: 这是一个React ref对象,指向要监听悬浮事件的DOM元素。使用ref是因为我们需要直接操作DOM元素来绑定和解绑事件监听器。
* enabled: 这是一个可选参数,默认为true。它允许使用者启用或禁用悬浮监听功能。这可以用来在某些条件下动态开启或关闭事件监听。
*/
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.');
}
}

// 判断是否处于悬浮状态的state
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;


// on函数 和 off函数 的封装
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 对象。
这个构造函数接受两个参数:一个回调函数和一个可选的配置对象。

  • 回调函数:当被观察的元素进入或退出交集区域时,回调函数会被执行。这个函数接受两个参数:

    • entries: 一个 IntersectionObserverEntry 对象数组,每个对象都描述了一个被观察元素的交集状态
    • observer: 对应的 IntersectionObserver 实例,允许你访问观察者的属性或调用其方法,如 disconnect 或 unobserve
  • 配置对象:包含三个属性

    • root
    • rootMargin
    • threshold

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';
/**
* useIntersection接受两个参数
* ref: 一个ref引用对象,指向要观察的目标元素
* options: 配置对象,用于初始化Intersection Observer的选项,比如 root、rootMargin 和 threshold
*/
const useIntersection = (
ref: RefObject<HTMLElement>,
options: IntersectionObserverInit
): IntersectionObserverEntry | null => {
// 创建 intersectionObserverEntry 状态,用于存储 intersectionObserverEntry 对象信息
const [intersectionObserverEntry, setIntersectionObserverEntry] =
useState<IntersectionObserverEntry | null>(null);

useEffect(() => {
// ref是否存在,浏览器是否支持IntersectionObserver
if (ref.current && typeof IntersectionObserver === 'function') {
// handle函数用于更新 intersectionObserverEntry 状态
const handler = (entries: IntersectionObserverEntry[]) => {
setIntersectionObserverEntry(entries[0]);
};

// 创建一个监听器对象
const observer = new IntersectionObserver(handler, options);
// 监听目标元素
observer.observe(ref.current);

// 组件销毁时取消监听,并清空 intersectionObserverEntry 状态
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>;
}
// createKeyPredicate接受一个参数keyFilter,该参数可以是一个判断函数(用于判断何时符合条件)、一个字符串(指定的keyboard) 或者 undefined/null
// 然后 createKeyPredicate 根据不同的参数来创建判断函数
// 如果传入的是函数,就返回这个函数
// 如果传入的是字符串(特定的keyboard),则将它包装成一个函数,当event.key === 字符串 时返回true,表示符合条件触发回调函数
// 如果传入的是undefined/null,则包装一个固定返回false的函数,表示不符合条件不触发回调函数
const createKeyPredicate = (keyFilter: KeyFilter): KeyPredicate =>
typeof keyFilter === 'function'
? keyFilter
: typeof keyFilter === 'string'
? (event: KeyboardEvent) => event.key === keyFilter
: keyFilter
? () => true
: () => false;
/**
*
* @param key 指定触发回调的键
* @param fn 要触发的回调函数
* @param opts 配置选项
* @param deps 依赖项
*/
const useKey = <T extends UseEventTarget>(
key: KeyFilter,
fn: Handler = noop,
opts: UseKeyOptions<T> = {},
deps: DependencyList = [key]
) => {
// event 指定是哪种键盘事件(keydown,keyup,keypress)
// target 绑定事件的目标对象
// options 传递给useEvent的配置项
const { event = 'keydown', target, options } = opts;

// 包装后的回调函数
const useMemoHandler = useMemo(() => {
// 根据传入的key值创建一个判断函数
const predicate: KeyPredicate = createKeyPredicate(key);
// handlerEvent 是浏览器事件对象,当向监听器添加事件处理函数时,浏览器会自动传入事件对象作为参数
// 我们可以通过event参数访问到事件的详细信息,如被按下的键是什么(event.key)
const handler: Handler = (handlerEvent) => {
// 判断是否符合触发回调的条件
if (predicate(handlerEvent)) {
// 如果符合,就执行用户传入的回调函数
return fn(handlerEvent);
}
};
return handler;
}, deps);

// 调用useEvent来为目标对象绑定事件
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 => {
// 检查浏览器传入的事件对象中是否有touches属性,即是否触发了touches事件
return 'touches' in ev;
};

const preventDefault = (ev: Event) => {
if (!isTouchEvent(ev)) return;

// 当触摸点个数小于2个时,阻止默认事件
if (ev.touches.length < 2 && ev.preventDefault) {
ev.preventDefault();
}
};
/**
*
* @param callback 长按之后的回调函数
* @param options 配置项对象(可选),其中包括是否阻止默认事件,延迟时间等
*/
const useLongPress = (
callback: (e: TouchEvent | MouseEvent) => void,
{ isPreventDefault = true, delay = 300 }: Options = {}
) => {
// 用于缓存 定时器id 和 目标对象
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;
}
// 缓存定时器id
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) {
// 从 entry.contentRect 中获取元素的尺寸位置信息
const { width, height } = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${width}px x ${height}px`);
}
});

// 假设有一个元素是这样的:<div id="myElement"></div>
const myElement = document.getElementById('myElement');

// 开始观察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> {
// element用于存储对DOM元素的引用
const [element, ref] = useState<E | null>(null);
// rect用于存储测量的结果
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]);

// 返回ref回调函数 可以将它绑定到元素的ref属性上,React会在组件挂载时将DOM元素作为参数传递给该ref回调函数
// 以及 rect对象(包含了元素的尺寸和位置信息)
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; // 鼠标的X坐标相对于整个文档(document)的位置
docY: number; // 鼠标的Y坐标相对于整个文档的位置
posX: number; // 目标元素的X坐标相对于其定位上下文(positioning context)的位置
posY: number; // 目标元素的Y坐标相对于其定位上下文的位置
elX: number; // 鼠标的X坐标相对于目标元素(offset parent)的位置
elY: number; // 鼠标的Y坐标相对于目标元素(offset parent)的位置
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) => {
// 如果已经绑定了元素的ref属性
if (ref && ref.current) {
const { left, top, width: elW, height: elH } = ref.current.getBoundingClientRect();
// window.pageXOffset 表示当前文档水平滚动的距离
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.relatedTargetevent.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);
// from 保存了鼠标即将移动到的元素
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'];

/**
* ref:目标对象的ref引用
* onClickAway:传入的回调函数
* events:需要绑定的事件类型
*/
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 是对DOM元素的引用
// 如果 el存在 并且 当前触发事件的元素不是el及其子孙节点 则 执行传入的回调函数
// contains 方法是用来测试一个节点是否是另一个节点的后代。如果目标元素在 el 之外,contains 方法会返回 false,取反后为 true
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;