react-use 部分源码分析

574 阅读4分钟

前言

2019 年写的分析,慎重观看。不过其中的思路还是值得学习的。

useBattery

useGeolocation

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

useLocation

  • 作用:获取浏览器location信息
  • 原理: 监听window的popstate,pushstate,replacestate事件。
  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 对象,然后添加监听事件。

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事件。
    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属性中保存着相关元素。

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。

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设置为不滚动,如期间再滚,清除计时器,重新开启计时器。

      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的方式,更多监听方式参考: xiangwenhu.github.io/TakeItEasy/…

  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等等。

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实例
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方法, 判断节点是否为该节点的后代节点。

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

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注入事件,然后在事件里面拦截,再调用传入的监听函数。

看看调用

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进行了额外的处理。


    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。 原生代码也就是

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

useWait

额,直接用的react-wait