react-use 源码分析

第一部分 Sensors 传感器?


useBattery

useGeolocation

  • 获取地址位置
  • Geolocation, navigator.geolocation.getCurrentPosition获取地址位置, navigator.geolocation.watchPosition监听变化。

useLocation

  • 作用:获取浏览器location信息
  • 原理: 监听window的popstate,pushstate,replacestate事件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
useEffect(() => {
const onPopstate = () => setState(buildState('popstate'));
const onPushstate = () => setState(buildState('pushstate'));
const onReplacestate = () => setState(buildState('replacestate'));

on(window, 'popstate', onPopstate);
on(window, 'pushstate', onPushstate);
on(window, 'replacestate', onReplacestate);

return () => {
off(window, 'popstate', onPopstate);
off(window, 'pushstate', onPushstate);
off(window, 'replacestate', onReplacestate);
};
}, []);

useMedia

使用window.matchMedia获得MediaQueryList 对象,然后添加监听事件。

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 { useEffect, useState } from 'react';
import { isClient } from './util';

const useMedia = (query: string, defaultState: boolean = false) => {
const [state, setState] = useState(isClient ? () => window.matchMedia(query).matches : defaultState);

useEffect(() => {
let mounted = true;
const mql = window.matchMedia(query);
const onChange = () => {
if (!mounted) {
return;
}
setState(!!mql.matches);
};

mql.addListener(onChange);
setState(mql.matches);

return () => {
mounted = false;
mql.removeListener(onChange);
};
}, [query]);

return state;
};

export default useMedia;

useNetwork

  1. 监听window的online和offline事件。
  2. 如果navigator支持connection,那么再监听change事件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    on(window, 'online', onOnline);
    on(window, 'offline', onOffline);
    if (connection) {
    on(connection, 'change', onConnectionChange);
    localSetState({
    ...state,
    online: navigator.onLine,
    since: undefined,
    ...getConnectionState(),
    });
    }

    return () => {
    off(window, 'online', onOnline);
    off(window, 'offline', onOffline);
    if (connection) {
    off(connection, 'change', onConnectionChange);
    }
    };

usePageLeave

document监听mouseout事件,判断event.relatedTarget || event.toElement 是不是documentElement。
DOM通过event对象的relatedTarget属性提供了相关元素的信息。这个属性只对于mouseover和mouseout事件才包含值;对于其他事件,这个属性的值是null。 IE不支持realtedTarget属性,但提供了保存着同样信息的不同属性。在mouseover事件触发时,IE的fromElement属性中保存了相关元素;在mouseout事件触发时,IE的toElement属性中保存着相关元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
}
};

document.addEventListener('mouseout', handler);
return () => {
document.removeEventListener('mouseout', handler);
};
}, args);
};

useIdle

  1. 监听 window的 [‘mousemove’, ‘mousedown’, ‘resize’, ‘keydown’, ‘touchstart’, ‘wheel’] 这些事件。 如果发生即为活跃。
  2. 监听document的onVisibility事件,如果是当前窗体激活,设置为活跃。
  3. 启动计时器,如果活跃之后一段时间没有相关操作,即为idle。
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

const defaultEvents = ['mousemove', 'mousedown', 'resize', 'keydown', 'touchstart', 'wheel'];
const oneMinute = 60e3;

const useIdle = (ms: number = oneMinute, initialState: boolean = false, events: string[] = defaultEvents): boolean => {
const [state, setState] = useState<boolean>(initialState);

useEffect(() => {
let mounted = true;
let timeout: any;
let localState: boolean = state;
const set = (newState: boolean) => {
if (mounted) {
localState = newState;
setState(newState);
}
};

const onEvent = throttle(50, () => {
if (localState) {
set(false);
}

clearTimeout(timeout);
timeout = setTimeout(() => set(true), ms);
});
const onVisibility = () => {
if (!document.hidden) {
onEvent();
}
};

for (let i = 0; i < events.length; i++) {
on(window, events[i], onEvent);
}
on(document, 'visibilitychange', onVisibility);

timeout = setTimeout(() => set(true), ms);

return () => {
mounted = false;

for (let i = 0; i < events.length; i++) {
off(window, events[i], onEvent);
}
off(document, 'visibilitychange', onVisibility);
};
}, [ms, events]);

return state;
};

useScrolling

计时器来实现,滚动事件后150ms设置为不滚动,如期间再滚,清除计时器,重新开启计时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  const handleScrollEnd = () => {
setScrolling(false);
};

const handleScroll = () => {
setScrolling(true);
clearTimeout(scrollingTimeout);
scrollingTimeout = setTimeout(() => handleScrollEnd(), 150);
};

ref.current.addEventListener('scroll', handleScroll, false);
return () => {
if (ref.current) {
ref.current.removeEventListener('scroll', handleScroll, false);
}
};
}

useSize

采取的内嵌iframe的方式,更多监听方式参考:
https://xiangwenhu.github.io/TakeItEasy/resize/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ref = useRef<HTMLIFrameElement | null>(null);
let window: Window | null = null;
const setSize = () => {
const iframe = ref.current;
const size = iframe
? {
width: iframe.offsetWidth,
height: iframe.offsetHeight,
}
: { width, height };

setState(size);
};
const onWindow = (windowToListenOn: Window) => {
windowToListenOn.addEventListener('resize', setSize);
DRAF(setSize);
};

useStartTyping

document注册keydown事件,检查事件触发时activeElement是不是input和textarea以及是不是有contenteditable属性,以及检查keyCode的值是不是在有效范围,排除ctl alt等等。

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
const isFocusedElementEditable = () => {
const { activeElement, body } = document;

if (!activeElement) {
return false;
}

// If not element has focus, we assume it is not editable, too.
if (activeElement === body) {
return false;
}

// Assume <input> and <textarea> elements are editable.
switch (activeElement.tagName) {
case 'INPUT':
case 'TEXTAREA':
return true;
}

// Check if any other focused element id editable.
return activeElement.hasAttribute('contenteditable');
};

const isTypedCharGood = ({ keyCode, metaKey, ctrlKey, altKey }: KeyboardEvent) => {
if (metaKey || ctrlKey || altKey) {
return false;
}
// 0...9
if (keyCode >= 48 && keyCode <= 57) {
return true;
}
// a...z
if (keyCode >= 65 && keyCode <= 90) {
return true;
}
// All other keys.
return false;
};

const useStartTyping = (onStartTyping: (event: KeyboardEvent) => void) => {
useLayoutEffect(() => {
const keydown = event => {
!isFocusedElementEditable() && isTypedCharGood(event) && onStartTyping(event);
};

document.addEventListener('keydown', keydown);
return () => {
document.removeEventListener('keydown', keydown);
};
}, []);
};

useMeasure

  1. ResizeObserver 采用了 resize-observer-polyfill, 高阶组件react-virtualized-auto-sizer 也能做到类似功能监听功能
  2. 利用函数ref属性
  3. useState使用函数参数初始化ResizeObserver实例
    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
    const useMeasure = <T>(): [(instance: T) => void, ContentRect] => {
    const [rect, set] = useState({
    width: 0,
    height: 0,
    top: 0,
    left: 0,
    bottom: 0,
    right: 0,
    });

    const [observer] = useState(
    () =>
    new ResizeObserver(entries => {
    const entry = entries[0];
    if (entry) {
    set(entry.contentRect);
    }
    })
    );

    const ref = useCallback(
    node => {
    observer.disconnect();
    if (node) {
    observer.observe(node);
    }
    },
    [observer]
    );
    return [ref, rect];
    };

    export default useMeasure;

useClickAway

HTML Nodecontains方法, 判断节点是否为该节点的后代节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const useClickAway = (
ref: RefObject<HTMLElement | null>,
onClickAway: (event: KeyboardEvent) => 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]);
};

useCss

借用了 CSS-in-JS library nano-css
nano-css本身使用的是 CSSStyleSheet.insertRule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const useCss = (css: object): string => {
const className = useMemo(() => 'react-use-css-' + (counter++).toString(36), []);
const sheet = useMemo(() => new nano.VSheet(), []);

useLayoutEffect(() => {
const tree = {};
cssToTree(tree, css, '.' + className, '');
sheet.diff(tree);

return () => {
sheet.diff({});
};
});

return className;
};

useDrop

监听document的dragover,dragenter,dragleave,dragexit,drop,paste等事件。

useDropArea

传入事件监听函数,返回bound各种方法以及是否hover属性。
boud方法需要挂载到节点,那么突破口就是bound。
通过bound注入事件,然后在事件里面拦截,再调用传入的监听函数。

看看调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {useDropArea} from 'react-use';

const Demo = () => {
const [bond, state] = useDropArea({
onFiles: files => console.log('files', files),
onUri: uri => console.log('uri', uri),
onText: text => console.log('text', text),
});

return (
<div {...bond}>
Drop something here.
</div>
);
};

useFullscreen

引入了库 screenfull
基本就是request全屏然后,监听change事件。
当然对video进行了额外的处理。

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

const onWebkitEndFullscreen = () => {
video!.current!.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
onClose();
};

const onChange = () => {
if (screenfull) {
const isScreenfullFullscreen = screenfull.isFullscreen;
setIsFullscreen(isScreenfullFullscreen);
if (!isScreenfullFullscreen) {
onClose();
}
}
};

if (screenfull && screenfull.enabled) {
try {
screenfull.request(ref.current);
setIsFullscreen(true);
} catch (error) {
onClose(error);
setIsFullscreen(false);
}
screenfull.on('change', onChange);
} else if (video && video.current && video.current.webkitEnterFullscreen) {
video.current.webkitEnterFullscreen();
video.current.addEventListener('webkitendfullscreen', onWebkitEndFullscreen);
setIsFullscreen(true);
} else {
onClose();
setIsFullscreen(false);
}

useSpeech

这个功能我以前不知道的说。
浏览器支持率居然将近 90%, Can i use |speechSynthesis
其实就用利用了window.speechSynthesis。
原生代码也就是

1
2
var utterance = new SpeechSynthesisUtterance('我们都是中国人'); 
window.speechSynthesis.speak(utterance)

useWait

额,直接用的react-wait