ahooks 源码解读系列 - 4

791 阅读2分钟

这个系列是将 ahooks 里面的所有 hook 源码都进行解读,通过解读 ahooks 的源码来熟悉自定义 hook 的写法,提高自己写自定义 hook 的能力,希望能够对大家有所帮助。

往期回顾

今天开始进入 UI 部分的 hooks,这部分的每个 hook 都感觉挺有用的,如果遇到对应场景,可以节省不少精力。

UI

useDrop & useDrag

一对帮助你处理在拖拽中进行数据转移的 hooks

type getDragPropsFn = (
  data: any,
) => {
  draggable: 'true';
  key: string;
  onDragStart: (e: React.DragEvent) => void;
  onDragEnd: (e: React.DragEvent) => void;
};

interface IConfig {
  onDragStart?: (data: any, e: React.DragEvent) => void;
  onDragEnd?: (data: any, e: React.DragEvent) => void;
}

const useDrag = (config?: IConfig): getDragPropsFn => {
  /// 方法每次重新渲染都会重新定义
  const getProps = (data: any) => {
    return {
      key: JSON.stringify(data),
      draggable: 'true' as const,/// 必须设置为 true 才能拖拽,html 属性设置为文本 "true"
      /// react 事件处理程序绑定方式,和 Dom 0 级事件绑定是不同的。https://react.docschina.org/docs/handling-events.html
      /// https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events#%E8%A1%8C%E5%86%85%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86%E5%99%A8_-_%E8%AF%B7%E5%8B%BF%E4%BD%BF%E7%94%A8
      onDragStart: (e: React.DragEvent) => {
        if (config && config.onDragStart) {
          config.onDragStart(data, e);
        }
        /// 通过自定义类型传递数据 https://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#drag-data-store-mode
        e.dataTransfer.setData('custom', JSON.stringify(data));
      },
      onDragEnd: (e: React.DragEvent) => {
        if (config && config.onDragEnd) {
          config.onDragEnd(data, e);
        }
      },
    };
  };

  return getProps;
};

export default useDrag;

import { useMemo, useState, useRef, useCallback } from 'react';

export interface DropAreaState {
  isHovering: boolean;
}

export interface DropProps {
  onDragOver: React.DragEventHandler;
  onDragEnter: React.DragEventHandler;
  onDragLeave: React.DragEventHandler;
  onDrop: React.DragEventHandler;
  onPaste: React.ClipboardEventHandler;
}

export interface DropAreaOptions {
  onFiles?: (files: File[], event?: React.DragEvent) => void;
  onUri?: (url: string, event?: React.DragEvent) => void;
  onDom?: (content: any, event?: React.DragEvent) => void;
  onText?: (text: string, event?: React.ClipboardEvent) => void;
}

const getProps = (
  callback: (dataTransfer: DataTransfer, event: React.DragEvent | React.ClipboardEvent) => void,
  setIsHovering: (over: boolean) => void,
): DropProps => ({
  onDragOver: (event: React.DragEvent) => {
    /// 在 dragenter (en-US) 或 dragover (en-US) 事件中调用  preventDefault() 方法将表明在该位置允许放置 。
    /// 否则无法触发 drop 事件
    /// https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#droptargets
    event.preventDefault();
  },
  onDragEnter: (event: React.DragEvent) => {
    event.preventDefault();
    setIsHovering(true);
  },
  onDragLeave: () => {
    setIsHovering(false);
  },
  onDrop: (event: React.DragEvent) => {
    /// 禁止默认行为,拖拽默认行为可能会打开新标签页等不希望产生的行为
    event.preventDefault();
    /// react 17 之前版本需要这行代码,防止 callback 中是异步操作
    /// https://zh-hans.reactjs.org/docs/legacy-event-pooling.html#gatsby-focus-wrapper
    /// 17版本该代码已经废弃 https://zh-hans.reactjs.org/docs/events.html#overview
    /// 该代码仍然可以运行但是不会有任何效果产生 https://zh-hans.reactjs.org/blog/2020/08/10/react-v17-rc.html#no-event-pooling
    event.persist();
    setIsHovering(false);
    callback(event.dataTransfer, event);
  },
  onPaste: (event: React.ClipboardEvent) => {
    event.persist();
    callback(event.clipboardData, event);/// 粘贴操作也触发 drop 的回调
  },
});

const useDrop = (options: DropAreaOptions = {}): [DropProps, DropAreaState] => {
  const optionsRef = useRef(options);
  optionsRef.current = options;
  const [isHovering, setIsHovering] = useState<boolean>(false);
  const callback = useCallback(
    (dataTransfer: DataTransfer, event: React.DragEvent | React.ClipboardEvent) => {
      const uri = dataTransfer.getData('text/uri-list'); /// 被拖拽的 a 标签的URL
      const dom = dataTransfer.getData('custom'); /// 这个自定义类型是 useDrag hook 定义的类型
      /// 使用了 custom 类型的情况,一般是使用了 useDrag
      if (dom && optionsRef.current.onDom) {
        let data = dom;
        try {
          data = JSON.parse(dom);
        } catch (e) {
          data = dom;
        }
        optionsRef.current.onDom(data, event as React.DragEvent);
        return;
      }
      /// 拖拽的是个链接,不是说文本内容是个链接,而是拖拽的是个 a 标签
      if (uri && optionsRef.current.onUri) {
        optionsRef.current.onUri(uri, event as React.DragEvent);
        return;
      }
      /// 如果操作中包含文件,则会有 files 字段 https://developer.mozilla.org/zh-CN/docs/Web/API/DataTransfer/files
      if (dataTransfer.files && dataTransfer.files.length && optionsRef.current.onFiles) {
        optionsRef.current.onFiles(Array.from(dataTransfer.files), event as React.DragEvent);
        return;
      }
      /// 其他情况下的文本操作,页面直接拖拽文本、粘贴文本 type: "text/plain"
      if (dataTransfer.items && dataTransfer.items.length && optionsRef.current.onText) {
        dataTransfer.items[0].getAsString((text) => {
          optionsRef.current.onText!(text, event as React.ClipboardEvent);
        });
      }
    },
    [],
  );

  const props: DropProps = useMemo(() => getProps(callback, setIsHovering), [
    callback,
    setIsHovering,
  ]);

  return [props, { isHovering }];
};

export default useDrop;

以上内容由于本人水平问题难免有误,欢迎大家进行讨论反馈。