Vue3 中不可错过的十大 Hook 汇总 | 让开发更高效

2,295 阅读8分钟

一、防抖和节流

防抖和节流的 Hook 函数

import { ref, watchEffect, onUnmounted } from 'vue';

/**
 * 防抖和节流的 Hook 函数
 * @param {Function} callback - 要执行的回调函数
 * @param {number} delay - 防抖或节流的延迟时间(毫秒)
 * @param {string} type - 'debounce' 或 'throttle',表示防抖或节流
 * @returns {Function} - 触发防抖或节流操作的函数
 */
function useDebounceThrottle(callback, delay = 500, type = 'debounce') {
  // 标志是否正在等待延迟执行
  let timer;
  // 标志节流的上次执行时间
  let lastExecuted;

  // 触发防抖或节流的函数
  function trigger() {
    // 根据类型进行不同的处理
    if (type === 'debounce') {
      // 防抖:清除之前的定时器
      clearTimeout(timer);
      timer = setTimeout(() => {
        callback();
      }, delay);
    } else if (type === 'throttle') {
      // 节流:如果距离上次执行超过延迟时间,则执行回调
      const now = Date.now();
      if (!lastExecuted || now - lastExecuted >= delay) {
        callback();
        lastExecuted = now;
      }
    }
  }

  // 组件卸载时清除定时器
  onUnmounted(() => {
    clearTimeout(timer);
  });

  // 返回触发函数
  return trigger;
}

使用方式

import { useDebounceThrottle } from './useDebounceThrottle'; // 引入自定义的 hook

export default {
  setup() {
    // 防抖示例
    const debounceTrigger = useDebounceThrottle(() => {
      console.log('Debounce triggered');
    }, 1000, 'debounce');

    // 模拟频繁触发防抖操作
    for (let i = 0; i < 10; i++) {
      debounceTrigger();
    }

    // 节流示例
    const throttleTrigger = useDebounceThrottle(() => {
      console.log('Throttle triggered');
    }, 2000, 'throttle');

    // 模拟频繁触发节流操作
    for (let i = 0; i < 10; i++) {
      throttleTrigger();
    }

    return {};
  },
};

二、获取鼠标位置

获取鼠标位置的 Hook 函数

import { onMounted, onUnmounted, ref } from 'vue';

// 定义获取鼠标位置的 hook
export function useMousePosition() {
  // 使用 ref 创建响应式变量来存储鼠标的 x 和 y 坐标
  const x = ref(0);
  const y = ref(0);

  // 鼠标移动事件处理函数
  const handleMouseMove = (event) => {
    x.value = event.pageX;
    y.value = event.pageY;
  };

  // 在组件挂载时添加鼠标移动事件监听
  onMounted(() => {
    window.addEventListener('mousemove', handleMouseMove);
  });

  // 在组件卸载前移除鼠标移动事件监听
  onUnmounted(() => {
    window.removeEventListener('mousemove', handleMouseMove);
  });

  // 返回包含 x 和 y 坐标的对象
  return { x, y };
}

使用方式

import { defineComponent } from 'vue';
import useMousePosition from './useMousePosition'; // 引入自定义的 hook

export default defineComponent({
  name: 'YourComponentName',
  setup() {
    // 调用 hook 函数获取鼠标位置
    const { x, y } = useMousePosition();

    // 这里可以使用 x 和 y 进行其他逻辑处理或在模板中使用
    return { x, y };
  },
});

三、监听窗口大小变化

监听窗口大小变化的 Hook 函数

import { onMounted, onUnmounted, ref } from "vue";

function useWindowResize() {
  // 使用 ref 创建响应式的 width 和 height 变量,用于存储窗口的宽度和高度
  const width = ref(0);
  const height = ref(0);

  // 定义一个处理窗口大小变化的函数
  function onResize() {
    width.value = window.innerWidth;
    height.value = window.innerHeight;
  }

  // 在组件挂载时,为 window 对象添加 resize 事件监听器
  onMounted(() => {
    window.addEventListener("resize", onResize);
    onResize(); // 立即获取一次窗口大小并更新 width 和 height 的值
  });

  // 在组件卸载时,移除 window 对象的 resize 事件监听器
  onUnmounted(() => {
    window.removeEventListener("resize", onResize);
  });

  // 返回包含 width 和 height 的对象,以便在组件中使用
  return { width, height };
}

export default useWindowResize;

使用方式

import useWindowResize from './useWindowResize'; 

export default {
  name: "app",
  setup() {
    // 调用 useWindowResize 函数获取 width 和 height 对象
    const { width, height } = useWindowResize(); 
    // 这里可以使用 width 和 height 进行其他逻辑处理或在模板中渲染
    return { width, height };
  }
};

四、缓存 / 预加载

缓存 / 预加载的 Hook 函数

import { ref, onMounted } from 'vue';

/**
 * 缓存/预加载的 hook
 * @param {Function} loadFunction - 用于加载数据的函数
 * @param {Object} options - 配置选项
 * @param {number} options.cacheTime - 缓存的有效时间(毫秒),默认 5 分钟
 * @returns {Object} - 包含加载状态、数据和加载函数的对象
 */
function useCachePreload(loadFunction, { cacheTime = 5 * 60 * 1000 } = {}) {
  // 加载状态
  const loading = ref(false);
  // 数据
  const data = ref(null);
  // 上次加载的时间
  const lastLoadTime = ref(0);

  /**
   * 加载数据的函数
   */
  const loadData = async () => {
    loading.value = true;
    try {
      // 如果缓存未过期,直接使用缓存数据
      if (Date.now() - lastLoadTime.value < cacheTime && data.value) {
        return;
      }
      const newData = await loadFunction();
      data.value = newData;
      lastLoadTime.value = Date.now();
    } catch (error) {
      console.error('加载数据出错:', error);
    } finally {
      loading.value = false;
    }
  };

  // 组件挂载时加载数据
  onMounted(loadData);

  // 返回相关数据和函数供组件使用
  return {
    loading,
    data,
    loadData
  };
}

export default useCachePreload;

使用方式

import useCachePreload from './useCachePreload';

export default {
  setup() {
    // 假设这是一个模拟的加载数据的函数
    const loadDataFunction = async () => {
      // 模拟加载数据的耗时操作
      await new Promise((resolve) => setTimeout(resolve, 2000));
      return { message: '加载成功的数据' };
    };

    const { loading, data, loadData } = useCachePreload(loadDataFunction);

    return { loading, data, loadData };
  }
}

五、媒体查询

媒体查询的 Hook 函数

import { ref, onMounted, onUnmounted } from 'vue';

/**
 * 用于封装媒体查询的钩子函数
 * @param {string} query - 媒体查询语句
 * @returns {boolean} - 媒体查询的匹配结果
 */
export const useMatchMedia = (query) => {
  // 创建媒体查询对象
  const mediaQuery = window.matchMedia(query);
  // 使用 ref 创建一个响应式变量来存储匹配结果
  const match = ref(mediaQuery.matches);

  // 定义一个在媒体查询状态改变时触发的回调函数
  const onChange = (e) => {
    match.value = e.matches;
  };

  // 使用 watchEffect 钩子在组件挂载时添加媒体查询事件监听器,并在组件卸载时移除
  onMounted(() => {
    mediaQuery.addEventListener('change', onChange);
  });

  onUnmounted(() => {
    mediaQuery.removeEventListener('change', onChange);
  });

  return match.value;
};

使用方式

import { useMatchMedia } from './useMatchMedia'; 

const isMobile = useMatchMedia('(max-width: 600px)'); 

// 在组件的模板或逻辑中使用 isMobile 变量
<div v-if="isMobile">这是在小屏幕(宽度小于 600px)下显示的内容</div>
<div v-else>这是在大屏幕下显示的内容</div>

在 onMounted 钩子中,添加了媒体查询的事件监听器,当媒体查询状态发生变化时,会触发 onChange 回调函数来更新 match 的值。在 onUnmounted 钩子中,移除了添加的事件监听器,以避免内存泄漏。

六、滚动相关操作和状态

滚动相关操作和状态的 Hook 函数

import { ref, onMounted, onUnmounted } from 'vue';

/**
 * 滚动相关操作和状态的 hook
 * @returns {Object} 包含滚动相关操作和状态的对象
 */
function useScroll() {
  // 滚动位置(垂直方向)
  const scrollTop = ref(0);
  // 是否滚动到底部
  const isBottom = ref(false);
  // 滚动方向,'up' 表示向上,'down' 表示向下,'none' 表示未滚动
  const scrollDirection = ref('none');
  // 记录上一次的滚动位置
  const prevScrollTop = ref(0);

  // 滚动到顶部的函数
  const scrollToTop = () => {
    window.scrollTo({
      top: 0,
      behavior:'smooth' 
    });
  };

  // 监听滚动事件
  const handleScroll = (event) => {
    scrollTop.value = window.pageYOffset || document.documentElement.scrollTop;

    const scrollHeight = event.target.scrollHeight;
    const clientHeight = event.target.clientHeight;
    if (scrollTop.value + clientHeight >= scrollHeight) { 
      isBottom.value = true;
    } else {
      isBottom.value = false;
    }

    // 判断滚动方向
    const currentScrollTop = scrollTop.value;
    if (currentScrollTop > prevScrollTop.value) {
      scrollDirection.value = 'down';
    } else if (currentScrollTop < prevScrollTop.value) {
      scrollDirection.value = 'up';
    } else {
      scrollDirection.value = 'none';
    }

    prevScrollTop.value = currentScrollTop;
  };

  // 组件挂载时添加滚动事件监听
  onMounted(() => {
    window.addEventListener('scroll', handleScroll);
  });

  // 组件卸载时移除滚动事件监听
  onUnmounted(() => {
    window.removeEventListener('scroll', handleScroll);
  });

  // 返回滚动相关的数据和操作
  return {
    scrollTop,
    isBottom,
    scrollDirection,
    scrollToTop
  };
}

export default useScroll;

使用方式

import useScroll from './useScroll';

export default {
  setup() {
    const { scrollTop, isBottom, scrollDirection, scrollToTop } = useScroll();

    // 可以在组件中根据滚动相关的数据和操作进行相应的处理
    // 例如,根据滚动位置显示或隐藏某些元素
    // 根据是否滚动到底部加载更多数据
    // 根据滚动方向执行特定的动画效果等

    return {
      scrollTop,
      isBottom,
      scrollDirection,
      scrollToTop
    };
  }
}

七、监听网络状态变化

监听网络状态变化的 Hook 函数

import { ref, onMounted, onUnmounted } from 'vue';

/**
 * 监听网络状态变化的 hook
 * @returns {Object} 包含网络状态和监听相关方法的对象
 */
function useNetworkStatus() {
  // 网络状态,初始为未知
  const networkStatus = ref('unknown');

  // 网络状态变化的处理函数
  const handleNetworkChange = () => {
    networkStatus.value = navigator.onLine? 'online' : 'offline';
  };

  // 组件挂载时添加网络状态变化的监听
  onMounted(() => {
    window.addEventListener('online', handleNetworkChange);
    window.addEventListener('offline', handleNetworkChange);
  });

  // 组件卸载时移除监听
  onUnmounted(() => {
    window.removeEventListener('online', handleNetworkChange);
    window.removeEventListener('offline', handleNetworkChange);
  });

  // 返回网络状态和相关方法
  return {
    networkStatus
  };
}

export default useNetworkStatus;

使用方式

import useNetworkStatus from './useNetworkStatus';

export default {
  setup() {
    const { networkStatus } = useNetworkStatus();

    // 在组件中根据网络状态进行相应的处理
    if (networkStatus.value === 'online') {
      // 网络在线时的操作
    } else if (networkStatus.value === 'offline') {
      // 网络离线时的操作
    }

    return { networkStatus };
  }
}

八、复制到剪贴板

复制到剪贴板的 Hook 函数

import { ref, onMounted, onUnmounted } from 'vue';

/**
 * 实现复制到剪贴板功能的 hook
 * @param {string} text - 要复制的文本
 * @returns {Object} 包含复制状态和复制操作的对象
 */
function useCopyToClipboard(text) {
  // 复制状态,初始为未复制
  const isCopied = ref(false);

  // 执行复制操作的函数
  const copyToClipboard = async () => {
    try {
      await navigator.clipboard.writeText(text);
      isCopied.value = true;
    } catch (err) {
      console.error('复制到剪贴板失败:', err);
    }
  };

  // 组件挂载时添加事件处理
  onMounted(() => {
    // 一段时间后将复制状态恢复为未复制,以避免一直显示已复制
    setTimeout(() => {
      isCopied.value = false;
    }, 3000);
  });

  // 返回复制状态和复制操作
  return {
    isCopied,
    copyToClipboard
  };
}

export default useCopyToClipboard;

使用方式

import useCopyToClipboard from './useCopyToClipboard';

export default {
  setup() {
    const textToCopy = '这是要复制的文本';
    const { isCopied, copyToClipboard } = useCopyToClipboard(textToCopy);

    // 在组件中根据 isCopied 的值进行相应的提示或处理
    return { isCopied, copyToClipboard };
  }
}

九、让元素具有可拖拽功能

让元素具有可拖拽功能的 Hook 函数

import { ref, onMounted, onUnmounted } from 'vue';

/**
 * 让元素具有可拖拽功能的 hook
 * @param {HTMLElement} element - 要设置为可拖拽的元素
 * @returns {Object} 包含拖拽相关状态和方法的对象
 */
function useDraggable(element) {
  // 初始时,元素的位置
  const initialPosition = { x: 0, y: 0 };

  // 鼠标按下时的位置
  const mouseDownPosition = { x: 0, y: 0 };

  // 元素是否正在被拖拽
  const isDragging = ref(false);

  // 鼠标按下事件处理函数
  const handleMouseDown = (event) => {
    event.preventDefault();
    isDragging.value = true;
    mouseDownPosition.x = event.clientX;
    mouseDownPosition.y = event.clientY;
    initialPosition.x = element.offsetLeft;
    initialPosition.y = element.offsetTop;
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // 鼠标移动事件处理函数
  const handleMouseMove = (event) => {
    if (isDragging.value) {
      const deltaX = event.clientX - mouseDownPosition.x;
      const deltaY = event.clientY - mouseDownPosition.y;
      element.style.left = initialPosition.x + deltaX + 'px';
      element.style.top = initialPosition.y + deltaY + 'px';
    }
  };

  // 鼠标抬起事件处理函数
  const handleMouseUp = () => {
    isDragging.value = false;
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('mouseup', handleMouseUp);
  };

  // 组件挂载时为元素添加鼠标按下事件监听
  onMounted(() => {
    element.addEventListener('mousedown', handleMouseDown);
  });

  // 组件卸载时移除事件监听
  onUnmounted(() => {
    element.removeEventListener('mousedown', handleMouseDown);
  });

  // 返回复制状态和复制操作
  return {
    isDragging
  };
}

export default useDraggable;

使用方式

import useDraggable from './useDraggable';

export default {
  setup() {
    const element = ref(null);
    const { isDragging } = useDraggable(element.value);

    return { element, isDragging };
  },
  template: `
    <div ref="element" style="width: 100px; height: 100px; background-color: red;">
    </div>
  `
}

十、文件下载

文件下载的 Hook 函数

import { ref } from 'vue';

/**
 * 文件下载的 hook
 * @param {Blob} blob - 要下载的 Blob 数据
 * @param {string} fileName - 下载文件的名称
 */
function useFileDownload(blob, fileName) {
  // 创建一个隐藏的 <a> 元素
  const link = ref(null);

  // 下载文件的函数
  const downloadFile = () => {
    if (link.value) {
      link.value.href = URL.createObjectURL(blob);
      link.value.download = fileName;
      link.value.click();

      // 释放 URL 对象,避免内存泄漏
      URL.revokeObjectURL(link.value.href);
    }
  };

  // 组件挂载时创建 <a> 元素
  onMounted(() => {
    link.value = document.createElement('a');
    link.value.style.display = 'none';
    document.body.appendChild(link.value);
  });

  // 组件卸载时移除 <a> 元素
  onUnmounted(() => {
    if (link.value) {
      document.body.removeChild(link.value);
    }
  });

  return downloadFile;
}

export default useFileDownload;

使用方式

import useFileDownload from './useFileDownload';

export default {
  setup() {
    // 假设获取到了 Blob 数据和文件名
    const blob = new Blob(['这是文件内容'], { type: 'text/plain' });
    const fileName = 'example.txt';

    const downloadFile = useFileDownload(blob, fileName);

    // 在需要下载的地方调用 downloadFile 函数
    downloadFile();

    return {};
  }
}