6

大家都能看得懂的源码 - 那些关于DOM的常见Hook封装(一) - 前端Gopal

 1 year ago
source link: https://www.cnblogs.com/gopal/p/16636977.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文是深入浅出 ahooks 源码系列文章的第十四篇,该系列已整理成文档-地址。觉得还不错,给个 star 支持一下哈,Thanks。

上一篇我们探讨了 ahooks 对 DOM 类 Hooks 使用规范,以及源码中是如何去做处理的。接下来我们就针对关于 DOM 的各个 Hook 封装进行解读。

useEventListener

优雅的使用 addEventListener。

我们先来看看 addEventListener 的定义,以下来自 MDN 文档:

EventTarget.addEventListener() 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。

这里的 EventTarget 可以是一个文档上的元素 Element,Document和Window 或者任何其他支持事件的对象 (比如 XMLHttpRequest)。

我们看 useEventListener 函数 TypeScript 定义,通过类型重载,它对 Element、Document、Window 等元素以及其事件名称和回调参数都做了定义。

function useEventListener<K extends keyof HTMLElementEventMap>(
  eventName: K,
  handler: (ev: HTMLElementEventMap[K]) => void,
  options?: Options<HTMLElement>,
): void;
function useEventListener<K extends keyof ElementEventMap>(
  eventName: K,
  handler: (ev: ElementEventMap[K]) => void,
  options?: Options<Element>,
): void;
function useEventListener<K extends keyof DocumentEventMap>(
  eventName: K,
  handler: (ev: DocumentEventMap[K]) => void,
  options?: Options<Document>,
): void;
function useEventListener<K extends keyof WindowEventMap>(
  eventName: K,
  handler: (ev: WindowEventMap[K]) => void,
  options?: Options<Window>,
): void;
function useEventListener(eventName: string, handler: noop, options: Options): void;

内部代码比较简单:

  • 判断是否支持 addEventListener,支持则将参数进行传递。可以留意注释中的几个参数的作用,当做复习,这里不展开细说。
  • useEffect 的返回逻辑,也就是组件卸载的时候,会自动清除事件监听器,避免产生内存泄露。
function useEventListener(
  // 事件名称
  eventName: string,
  // 处理函数
  handler: noop,
  // 设置
  options: Options = {},
) {
  const handlerRef = useLatest(handler);

  useEffectWithTarget(
    () => {
      const targetElement = getTargetElement(options.target, window);
      if (!targetElement?.addEventListener) {
        return;
      }

      const eventListener = (event: Event) => {
        return handlerRef.current(event);
      };

      // 监听事件
      targetElement.addEventListener(eventName, eventListener, {
        // listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
        capture: options.capture,
        // listener 在添加之后最多只调用一次。如果是 true,listener 会在其被调用之后自动移除。
        once: options.once,
        // 设置为 true 时,表示 listener 永远不会调用 preventDefault() 。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告
        passive: options.passive,
      });

      // 移除事件
      return () => {
        targetElement.removeEventListener(eventName, eventListener, {
          capture: options.capture,
        });
      };
    },
    [eventName, options.capture, options.once, options.passive],
    options.target,
  );
}

useClickAway

监听目标元素外的点击事件。

提到这个的应用场景,应该是模态框,点击外部阴影部分,自动关闭的场景。那这里它是怎么实现的呢?

首先它支持传递 DOM 节点或者 Ref,并且是支持数组方式。
事件默认是支持 click,开发者可以自行传递并支持数组方式。

export default function useClickAway<T extends Event = Event>(
  // 触发函数
  onClickAway: (event: T) => void,
  // DOM 节点或者 Ref,支持数组
  target: BasicTarget | BasicTarget[],
  // 指定需要监听的事件,支持数组
  eventName: string | string[] = 'click',
) {
}

然后内部通过 document.addEventListener 监听事件。组件卸载的时候清除事件监听。

// 事件列表
const eventNames = Array.isArray(eventName) ? eventName : [eventName];
// document.addEventListener 监听事件,通过事件代理的方式知道目标节点
eventNames.forEach((event) => document.addEventListener(event, handler));
return () => {
  eventNames.forEach((event) => document.removeEventListener(event, handler));
};

最后看 handler 函数,通过 event.target 获取到触发事件的对象 (某个 DOM 元素) 的引用,判断假如不在传入的 target 列表中,则触发定义好的 onClickAway 函数。

const handler = (event: any) => {
  const targets = Array.isArray(target) ? target : [target];
  if (
    // 判断点击的 DOM Target 是否在定义的 DOM 元素(列表)中
    targets.some((item) => {
      const targetElement = getTargetElement(item);
      return !targetElement || targetElement.contains(event.target);
    })
  ) {
    return;
  }
  // 触发点击事件
  onClickAwayRef.current(event);
};

小结一下,useClickAway 就是使用了事件代理的方式,通过 document 监听事件,判断触发事件的 DOM 元素是否在 target 列表中,从而决定是否要触发定义好的函数。

useEventTarget

常见表单控件(通过 e.target.value 获取表单值) 的 onChange 跟 value 逻辑封装,支持自定义值转换和重置功能。

直接看代码,比较简单,其实就是监听表单的 onChange 事件,拿到值后更新 value 值,更新的逻辑支持自定义。

function useEventTarget<T, U = T>(options?: Options<T, U>) {
  const { initialValue, transformer } = options || {};
  const [value, setValue] = useState(initialValue);
  // 自定义转换函数
  const transformerRef = useLatest(transformer);
  const reset = useCallback(() => setValue(initialValue), []);
  const onChange = useCallback((e: EventTarget<U>) => {
    // 获取 e.target.value 的值,并进行设置
    const _value = e.target.value;
    if (isFunction(transformerRef.current)) {
      return setValue(transformerRef.current(_value));
    }
    // no transformer => U and T should be the same
    return setValue(_value as unknown as T);
  }, []);

  return [
    value,
    {
      onChange,
      reset,
    },
  ] as const;
}

useTitle

用于设置页面标题。

这个页面标题指的是浏览器 Tab 中展示的。通过 document.title 设置。

0fcad6992aa3459497c4d1dbd16e64a4~tplv-k3u1fbpfcp-zoom-1.image

代码非常简单,一看就会:

function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
  const titleRef = useRef(isBrowser ? document.title : '');
  useEffect(() => {
    document.title = title;
  }, [title]);

  useUnmount(() => {
    // 组件卸载后,恢复上一次的 title
    if (options.restoreOnUnmount) {
      document.title = titleRef.current;
    }
  });
}

useFavicon

设置页面的 favicon。

favicon 指的是页面 Tab 的这个 ICON。

9897604c441943e88e62d188df98ffc0~tplv-k3u1fbpfcp-zoom-1.image

原理是通过 link 标签设置 favicon。

const useFavicon = (href: string) => {
  useEffect(() => {
    if (!href) return;

    const cutUrl = href.split('.');
    const imgSuffix = cutUrl[cutUrl.length - 1].toLocaleUpperCase() as ImgTypes;

    const link: HTMLLinkElement =
      document.querySelector("link[rel*='icon']") || document.createElement('link');
    // 用于定义链接的内容的类型。
    link.type = ImgTypeMap[imgSuffix];
    // 指定被链接资源的URL。
    link.href = href;
    // 此属性命名链接文档与当前文档的关系。
    link.rel = 'shortcut icon';

    document.getElementsByTagName('head')[0].appendChild(link);
  }, [href]);
};

本文已收录到个人博客中,欢迎关注~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK