ahooks serie de interpretación de código fuente - 12

Esta serie es para interpretar todo el código fuente de ganchos en ahoos. Al interpretar el código fuente de ahoos, puede familiarizarse con la forma de escribir ganchos personalizados, mejorar su capacidad para escribir ganchos personalizados y esperar ser útil para todos.

Para distinguirlo de los comentarios originales del código, la parte de comprensión personal ///comienza con , que no tiene nada que ver con la instrucción de tres barras aquí , solo por distinción.

revisión anterior

Todos tuvieron un buen fin de semana, ¿verdad? ¿Todavía estás un poco sin trabajo al comienzo de la nueva semana? Entonces echemos un vistazo al código fuente de ahooks~ Hoy, ingresaré los ganchos en la parte Dom. Hay 16 ganchos en la parte Dom, que encapsulan varios escenarios comunes relacionados con Dom.

Juicio

useEventTarget

v-modelo yyds

/// ...

function useEventTarget<T, U = T>(options?: Options<T, U>) {
  const { initialValue, transformer } = options || {};
  const [value, setValue] = useState(initialValue);

  const reset = useCallback(() => setValue(initialValue), []);

  const transformerRef = useRef(transformer);
  transformerRef.current = transformer;

  const onChange = useCallback((e: EventTarget<U>) => {
    /// 默认情况下取值,如果是 checkbox 就需要自己写 transformer 了
    const _value = e.target.value;
    if (typeof transformerRef.current === 'function') {
      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;
}

export default useEventTarget;

复制代码

usarEventListener

No considere el detector de eventos para la destrucción

import { MutableRefObject } from 'react';

export type BasicTarget<T = HTMLElement> =
  | (() => T | null)
  | T
  | null
  | MutableRefObject<T | null | undefined>;

type TargetElement = HTMLElement | Element | Document | Window;

export function getTargetElement(
  target?: BasicTarget<TargetElement>,
  defaultElement?: TargetElement,
): TargetElement | undefined | null {
  /// target不存在,取默认元素
  if (!target) {
    return defaultElement;
  }

  let targetElement: TargetElement | undefined | null;
  /// 如果是方法就运行一下,如果有 current 属性值则取 current(针对Ref),否则直接去入参
  if (typeof target === 'function') {
    targetElement = target();
  } else if ('current' in target) {
    targetElement = target.current;
  } else {
    targetElement = target;
  }

  return targetElement;
}

复制代码
import { useEffect, useRef } from 'react';
import { BasicTarget, getTargetElement } from '../utils/dom';

/// ...

function useEventListener(eventName: string, handler: Function, options: Options = {}) {
  const handlerRef = useRef<Function>();
  handlerRef.current = handler;
  /// 组件渲染之后绑定事件,销毁时清除绑定
  useEffect(() => {
    const targetElement = getTargetElement(options.target, window)!;
    if (!targetElement.addEventListener) {
      return;
    }

    const eventListener = (
      event: Event,
    ): EventListenerOrEventListenerObject | AddEventListenerOptions => {
      return handlerRef.current && handlerRef.current(event);
    };

    targetElement.addEventListener(eventName, eventListener, {
      capture: options.capture,
      once: options.once,
      passive: options.passive,
    });

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

export default useEventListener;

复制代码

useKeyPress

"Manejando eventos de teclado, somos profesionales"

Un gancho que maneja específicamente eventos de teclado, que puede manejar fácilmente combinaciones de teclas.

import { useEffect, useRef } from 'react';
import { BasicTarget, getTargetElement } from '../utils/dom';

/// ...

// 键盘事件 keyCode 别名
const aliasKeyCodeMap: any = {
  esc: 27,
  tab: 9,
  enter: 13,
  space: 32,
  up: 38,
  left: 37,
  right: 39,
  down: 40,
  delete: [8, 46],
};

// 键盘事件 key 别名
const aliasKeyMap: any = {
  esc: 'Escape',
  tab: 'Tab',
  enter: 'Enter',
  space: ' ',
  // IE11 uses key names without `Arrow` prefix for arrow keys.
  up: ['Up', 'ArrowUp'],
  left: ['Left', 'ArrowLeft'],
  right: ['Right', 'ArrowRight'],
  down: ['Down', 'ArrowDown'],
  delete: ['Backspace', 'Delete'],
};

// 修饰键
const modifierKey: any = {
  ctrl: (event: KeyboardEvent) => event.ctrlKey,
  shift: (event: KeyboardEvent) => event.shiftKey,
  alt: (event: KeyboardEvent) => event.altKey,
  meta: (event: KeyboardEvent) => event.metaKey,
};

/// 不是返回空对象,是定义了一个空函数,返回的是 undefined
// 返回空对象
const noop = () => {};

/**
 * 判断对象类型
 * @param [obj: any] 参数对象
 * @returns String
 */
function isType(obj: any) {
  return Object.prototype.toString
    .call(obj)
    .replace(/^\[object (.+)\]$/, '$1')
    .toLowerCase();
}

/**
 * 判断按键是否激活
 * @param [event: KeyboardEvent]键盘事件
 * @param [keyFilter: any] 当前键
 * @returns Boolean
 */
function genFilterKey(event: any, keyFilter: any) {
  // 浏览器自动补全 input 的时候,会触发 keyDown、keyUp 事件,但此时 event.key 等为空
  if (!event.key) {
    return false;
  }

  const type = isType(keyFilter);
  // 数字类型直接匹配事件的 keyCode
  if (type === 'number') {
    return event.keyCode === keyFilter;
  }
  // 字符串依次判断是否有组合键
  const genArr = keyFilter.split('.');
  let genLen = 0;
  for (const key of genArr) {
    // 组合键
    const genModifier = modifierKey[key];
    // key 别名
    const aliasKey = aliasKeyMap[key];
    // keyCode 别名
    const aliasKeyCode = aliasKeyCodeMap[key];
    /**
     * 满足以上规则
     * 1. 自定义组合键别名
     * 2. 自定义 key 别名
     * 3. 自定义 keyCode 别名
     * 4. 匹配 key 或 keyCode
     */
    if (
      (genModifier && genModifier(event)) ||
      (aliasKey && isType(aliasKey) === 'array'
        ? aliasKey.includes(event.key)
        : aliasKey === event.key) ||
      (aliasKeyCode && isType(aliasKeyCode) === 'array'
        ? aliasKeyCode.includes(event.keyCode)
        : aliasKeyCode === event.keyCode) ||
      event.key.toUpperCase() === key.toUpperCase()
    ) {
      genLen++;
    }
  }
  return genLen === genArr.length;
}

/**
 * 键盘输入预处理方法
 * @param [keyFilter: any] 当前键
 * @returns () => Boolean
 */
function genKeyFormater(keyFilter: any): KeyPredicate {
  const type = isType(keyFilter);
  if (type === 'function') {
    return keyFilter;
  }
  if (type === 'string' || type === 'number') {
    return (event: KeyboardEvent) => genFilterKey(event, keyFilter);
  }
  /// 如果是数组,则只要有一个符合就可以触发
  if (type === 'array') {
    return (event: KeyboardEvent) => keyFilter.some((item: any) => genFilterKey(event, item));
  }
  return keyFilter ? () => true : () => false;
}

const defaultEvents: Array<keyEvent> = ['keydown'];

function useKeyPress(
  keyFilter: KeyFilter,
  eventHandler: EventHandler = noop,
  option: EventOption = {},
) {
  const { events = defaultEvents, target } = option;
  const callbackRef = useRef(eventHandler);
  callbackRef.current = eventHandler;

  useEffect(() => {
    const callbackHandler = (event) => {
      /// 如果事件对象符合传入的组合键要求则触发回调
      const genGuard: KeyPredicate = genKeyFormater(keyFilter);
      if (genGuard(event)) {
        return callbackRef.current(event);
      }
    };

    const el = getTargetElement(target, window)!;
    /// 默认绑定在 keyDown 事件上,可以自定义
    for (const eventName of events) {
      el.addEventListener(eventName, callbackHandler);
    }
    return () => {
      for (const eventName of events) {
        el.removeEventListener(eventName, callbackHandler);
      }
    };
  }, [events, keyFilter, target]);
}

export default useKeyPress;

复制代码

useScroll

"Tratando con el desplazamiento, también soy un profesional"

import { useEffect, useState } from 'react';
import usePersistFn from '../usePersistFn';
import { BasicTarget, getTargetElement } from '../utils/dom';

/// ...

function useScroll(target?: Target, shouldUpdate: ScrollListenController = () => true): Position {
  const [position, setPosition] = useState<Position>({
    left: NaN,
    top: NaN,
  });

  const shouldUpdatePersist = usePersistFn(shouldUpdate);

  useEffect(() => {
    const el = getTargetElement(target, document);
    if (!el) return;

    function updatePosition(currentTarget: Target): void {
      let newPosition;
      if (currentTarget === document) {
        if (!document.scrollingElement) return;
        newPosition = {
          left: document.scrollingElement.scrollLeft,
          top: document.scrollingElement.scrollTop,
        };
      } else {
        newPosition = {
          left: (currentTarget as HTMLElement).scrollLeft,
          top: (currentTarget as HTMLElement).scrollTop,
        };
      }
      if (shouldUpdatePersist(newPosition)) setPosition(newPosition);
    }

    updatePosition(el as Target);

    function listener(event: Event): void {
      if (!event.target) return;
      updatePosition(event.target as Target);
    }
    el.addEventListener('scroll', listener);
    return () => {
      el.removeEventListener('scroll', listener);
    };
  }, [target, shouldUpdatePersist]);

  return position;
}

export default useScroll;

复制代码

en tamaño

Obtenga los últimos datos de tamaño del objetivo en tiempo real, según la API ResizeObserver

import { useState, useLayoutEffect } from 'react';
import ResizeObserver from 'resize-observer-polyfill';
import { getTargetElement, BasicTarget } from '../utils/dom';

type Size = { width?: number; height?: number };

function useSize(target: BasicTarget): Size {
  const [state, setState] = useState<Size>(() => {
    const el = getTargetElement(target);
    return {
      width: ((el || {}) as HTMLElement).clientWidth,
      height: ((el || {}) as HTMLElement).clientHeight,
    };
  });
  /// 需要拿到 dom 数据,所以使用了 useLayoutEffect
  useLayoutEffect(() => {
    const el = getTargetElement(target);
    if (!el) {
      return () => {};
    }
    /// https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver
    const resizeObserver = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        setState({
          width: entry.target.clientWidth,
          height: entry.target.clientHeight,
        });
      });
    });

    resizeObserver.observe(el as HTMLElement);
    return () => {
      resizeObserver.disconnect();
    };
  }, [target]);

  return state;
}

export default useSize;

复制代码

usarClickAway

las ventanas emergentes modales se usan a menudo

import { useEffect, useRef } from 'react';
import { BasicTarget, getTargetElement } from '../utils/dom';

// 鼠标点击事件,click 不会监听右键
const defaultEvent = 'click';

type EventType = MouseEvent | TouchEvent;

export default function useClickAway(
  onClickAway: (event: EventType) => void,
  target: BasicTarget | BasicTarget[],
  eventName: string = defaultEvent,
) {
  const onClickAwayRef = useRef(onClickAway);
  onClickAwayRef.current = onClickAway;

  useEffect(() => {
    const handler = (event: any) => {
      const targets = Array.isArray(target) ? target : [target];
      if (
        targets.some((targetItem) => {
          const targetElement = getTargetElement(targetItem) as HTMLElement;
          /// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/contains
          /// Node.contains()返回的是一个布尔值,来表示传入的节点是否是 node 的后代节点或是 node 节点本身
          return !targetElement || targetElement?.contains(event.target);
        })
      ) {
        return;
      }
      /// 一个目标都没有命中,则触发回调
      onClickAwayRef.current(event);
    };

    document.addEventListener(eventName, handler);

    return () => {
      document.removeEventListener(eventName, handler);
    };
  }, [target, eventName]);
}

复制代码

Referencias

El contenido anterior es inevitablemente incorrecto debido a mi nivel de problemas, invitamos a todos a discutir y comentar.

Supongo que te gusta

Origin juejin.im/post/7002037636454055944
Recomendado
Clasificación