封装常用的工具函数助力业务开发(含react hooks/vue hooks)

哈喽,我是柠檬酱👏

工欲善其事必先利其器,咱们来扒一扒开源库 qiankun/micro app/icestark/wujie/ahooks/hooks等 用到的一些工具函数

数据类型判断

基本数据类型除了null都可以通过typeof判断

Object.toString.call可以判断所有的数据类型

判断所有的数据类型

typescript 复制代码
type AllTypes =
  | 'Number'
  | 'Object'
  | 'BigInt'
  | 'Boolean'
  | 'Undefined'
  | 'Null'
  | 'Array'
  | 'Symbol'
  | 'Math'
  | 'JSON'
  | 'Date'
  | 'RegExp'
  | 'Error'
  | 'Window'
  | 'HTMLDocument';

const checkTypes = (type: AllTypes) => <T>(value: unknown): value is T => Object.prototype.toString.call(value).slice(8, -1) === type;

是否为字符串

csharp 复制代码
 function isString (target: unknown): target is string {
  return typeof target === 'string'
}

是否为null

csharp 复制代码
function isNull (target: unknown): target is null {
  return target === null
}

是否为undefined

javascript 复制代码
function isUndefined (target: unknown): target is undefined {
  return target === undefined
}

是否为布尔值

vbnet 复制代码
function isBoolean (target: unknown): target is boolean {
  return typeof target === 'boolean'
}

是否为数字类型

csharp 复制代码
function isNumber (target: unknown): target is Number {
  return typeof target === 'number'
}

是否为函数

vbnet 复制代码
function isFunction (target: unknown): target is Function {
  return typeof target === 'function'
}

是否为对象

vbnet 复制代码
function isObject (target: unknown): target is Object {
  return typeof target === 'object'
}

是否为promise

typescript 复制代码
const toString = Object.prototype.toString
function isPromise (target: unknown): target is Promise<unknown> {
  return toString.call(target) === '[object Promise]'
}

是否为plainObject

plainObject和普通对象有什么区别?

通过对象字面量或new Object创建的对象叫plainObject

typeof null === object但是它不是plainObject

typescript 复制代码
// micro app中isPlainObject判断
const toString = Object.prototype.toString
function isPlainObject <T = Record<PropertyKey, unknown>> (target: unknown): target is T {
  return toString.call(target) === '[object Object]'
}

// react中plainObject判断
function isPlainObject(target){
 if(target ===null || typeof target !== object){
   return false
 }
}

是否为构造函数

拆解下面的正则表达式:

matlab 复制代码
/^function\s+[A-Z]/.test(targetStr)
  • \s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符,记忆:s代表space,空格
  • +为量词表示{1,},至少出现1次
  • A-Z\]表示大写字母A到Z

arduino 复制代码
/^class\s+/.test(targetStr)

这段正则表明以class开头命名,至少有一个空格的字符串

typescript 复制代码
function isConstructor (target: unknown): boolean {
  // 构造函数的前提:它本身是一个函数
  if (isFunction(target)) {
    // 函数转字符串
    const targetStr = target.toString()
    return (
      target.prototype?.constructor === target &&
      Object.getOwnPropertyNames(target.prototype).length > 1
    ) ||
      /^function\s+[A-Z]/.test(targetStr) ||
      /^class\s+/.test(targetStr)
  }
  // 不是函数返回false
  return false
}

浏览器检测

检测浏览器类型通过navigator.userAgent判断即可

是否为谷歌浏览器

javascript 复制代码
function isChomre():boolean{
 return /Chrome/.test(navigator.userAgent)
}

是否为safari浏览器

javascript 复制代码
function isSafari (): boolean {
  return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)
}

是否为火狐浏览器

arduino 复制代码
function isFireFox (): boolean {
  return navigator.userAgent.indexOf('Firefox') > -1
}

文件类型判断

是否为内联js

php 复制代码
function isInlineScript (address: string): boolean {
  return address.startsWith('inline-')
}

shadowDOM判断

csharp 复制代码
function isShadowRoot (target: unknown): target is ShadowRoot {
  return typeof ShadowRoot !== 'undefined' && target instanceof ShadowRoot
}

<link>/<style>/<script>/<iframe>/<div>/<img>标签都是通过元素的tagName判断

link标签

csharp 复制代码
function isLinkElement (target: unknown): target is HTMLLinkElement {
  return (target as HTMLLinkElement)?.tagName?.toUpperCase() === 'LINK'
}

style标签

csharp 复制代码
function isStyleElement (target: unknown): target is HTMLStyleElement {
  return (target as HTMLStyleElement)?.tagName?.toUpperCase() === 'STYLE'
}

script标签

csharp 复制代码
function isScriptElement (target: unknown): target is HTMLScriptElement {
  return (target as HTMLScriptElement)?.tagName?.toUpperCase() === 'SCRIPT'
}

iframe标签

csharp 复制代码
 function isIFrameElement (target: unknown): target is HTMLIFrameElement {
  return (target as HTMLIFrameElement)?.tagName?.toUpperCase() === 'IFRAME'
}

是否为div元素

csharp 复制代码
function isDivElement (target: unknown): target is HTMLDivElement {
  return (target as HTMLDivElement)?.tagName?.toUpperCase() === 'DIV'
}

是否为img元素

csharp 复制代码
function isImageElement (target: unknown): target is HTMLImageElement {
  return (target as HTMLImageElement)?.tagName?.toUpperCase() === 'IMG'
}

是否为URL

php 复制代码
function isURL (target: unknown): target is URL {
  return target instanceof URL || !!(target as URL)?.href
}

是否支持scope css

javascript 复制代码
// qiankun写法
export type FrameworkConfiguration = QiankunSpecialOpts & ImportEntryOpts & StartOpts;
function isEnableScopedCSS(sandbox: FrameworkConfiguration['sandbox']) {
  if (typeof sandbox !== 'object') {
    return false;
  }

  if (sandbox.strictStyleIsolation) {
    return false;
  }

  return !!sandbox.experimentalStyleIsolation;
}

URL处理

给url增加协议名

globalThis是通用的写法(兼容浏览器和Node),浏览器环境使用window.location.protocol即可

typescript 复制代码
function addProtocol (url: string): string {
  return url.startsWith('//') ? `${globalThis.location.protocol}${url}` : url
}

查询对象转字符串

用途:http请求处理

csharp 复制代码
function stringifyQuery (queryObject: LocationQueryObject): string {
  let result = ''

  for (const key in queryObject) {
    const value = queryObject[key]
    if (isNull(value)) {
      result += (result.length ? '&' : '') + key
    } else {
      const valueList: LocationQueryValue[] = isArray(value) ? value : [value]

      valueList.forEach(value => {
        if (!isUndefined(value)) {
          result += (result.length ? '&' : '') + key
          if (!isNull(value)) result += '=' + value
        }
      })
    }
  }

  return result
}

去空格

typescript 复制代码
 function trim (str: string): string {
  return str ? str.replace(/^\s+|\s+$/g, '') : ''
}

nextTick

javascript 复制代码
function nextTick(cb: () => any): void {
  Promise.resolve().then(cb);
}

sleep

面试官:实现一个sleep函数,要求等待指定的时机再执行

javascript 复制代码
// ms为毫秒数
function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

执行钩子函数

plugins为多个钩子函数(数组),遍历plugins,保留plugin[hookName]存在的且为函数类型的,将传参arg传入单个plugin依次执行

scss 复制代码
function execHooks(plugins: Array<plugin>, hookName: string, ...args: Array<any>): void {
  try {
    if (plugins && plugins.length > 0) {
      plugins
        .map((plugin) => plugin[hookName])
        .filter((hook) => isFunction(hook))
        .forEach((hook) => hook(...args));
    }
  } catch (e) {
    error(e);
  }
}

requestIdleCallback

兼容处理

javascript 复制代码
// wujie写法
 const requestIdleCallback = window.requestIdleCallback || ((cb: Function) => setTimeout(cb, 1));

函数柯里化

f1, f2, f3, f4\] =\> f4(f3(f2(f1))) ```typescript function compose(fnList: Array): (...args: Array) => string { return function (code: string, ...args: Array) { return fnList.reduce((newCode, fn) => (isFunction(fn) ? fn(newCode, ...args) : newCode), code || ""); }; } ``` **事件触发器** ```csharp function eventTrigger(el: HTMLElement | Window | Document, eventName: string, detail?: any) { let event; if (typeof window.CustomEvent === "function") { event = new CustomEvent(eventName, { detail }); } else { event = document.createEvent("CustomEvent"); event.initCustomEvent(eventName, true, false, detail); } el.dispatchEvent(event); } ``` **元素查找** 从数组list中查找element是否存在 ```javascript function find(list, element) { if (!Array.isArray(list)) { return false; } return list.filter((item) => item === element).length > 0; } ``` **获取元素** ```javascript function getContainer(container: string | HTMLElement): HTMLElement | null { return typeof container === 'string' ? document.querySelector(container) : container; } ``` 页面元素如下图 ![image.png](https://file.jishuzhan.net/article/1735586984971210754/5970522bc281b62c15233a3c0aafd6c3.webp) ![image.png](https://file.jishuzhan.net/article/1735586984971210754/b672e4fd36382fcfcc0c6ffe9d8f2cb3.webp) ```ini const asyncForEach = async(arr, callback)=> { for (let idx = 0; idx < arr.length; ++idx) { await callback(arr[idx], idx); } }; ``` ## 日志处理 **告警日志处理** ```typescript function warn(msg: string, data?: any): void { console?.warn(`[wujie warn]: ${msg}`, data); } ``` **错误日志处理** ```typescript function error(msg: string, data?: any): void { console?.error(`[wujie error]: ${msg}`, data); } ``` **全局错误处理** 分别绑定错误处理事件监听、移除错误处理事件监听 ```javascript export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void { window.addEventListener('error', errorHandler); window.addEventListener('unhandledrejection', errorHandler); } export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) { window.removeEventListener('error', errorHandler); window.removeEventListener('unhandledrejection', errorHandler); } ``` ## react专用 ### HOC 高阶组件是一个函数,传入的参数是组件,返回值也是组件 React.cloneElement是一个组件增强方法,支持拓展props和state ```javascript function renderComponent(Component: any, props = {}): React.ReactElement { return React.isValidElement(Component) ? ( React.cloneElement(Component, props) ) : ( ); } ``` ### hooks 下面代码片段来自阿里的ahooks库,具体使用参考 [ahooks.js.org/zh-CN/hooks...](https://link.juejin.cn?target=https%3A%2F%2Fahooks.js.org%2Fzh-CN%2Fhooks%2Fuse-request%2Findex "https://ahooks.js.org/zh-CN/hooks/use-request/index") **useLatest** 返回当前最新值的 Hook,可以避免闭包问题 useLatest返回的永远是最新值 ```csharp import { useRef } from 'react'; function useLatest(value: T) { const ref = useRef(value); ref.current = value; return ref; } ``` **useMount** 在组件挂载阶段执行的函数 ```javascript import { useEffect } from 'react'; const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; const useMount = (fn: () => void) => { if (isDev) { if (typeof fn !== 'function') { console.error( `useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`, ); } } useEffect(() => { fn?.(); }, []); }; ``` **useUnmount** 在组件卸载时执行的函数 ```javascript import { useEffect } from 'react'; const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; const useUnmount = (fn: () => void) => { if (isDev) { if (typeof fn !== 'function') { console.error(`useUnmount expected parameter is a function, got ${typeof fn}`); } } const fnRef = useLatest(fn); useEffect( () => () => { fnRef.current(); }, [], ); }; ``` **useDebounce** 防抖 ```typescript import {useState,useEffect,useMemo} from "react"; import { debounce } from 'lodash'; const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; export interface DebounceOptions { wait?: number; leading?: boolean; trailing?: boolean; maxWait?: number; } function useDebounceFn(fn: T, options?: DebounceOptions) { if (isDev) { if (typeof fn !== 'function') { console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`); } } const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const debounced = useMemo( () => debounce( (...args: Parameters): ReturnType => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { debounced.cancel(); }); return { run: debounced, cancel: debounced.cancel, flush: debounced.flush, }; } export function useDebounce(value: T, options?: DebounceOptions) { const [debounced, setDebounced] = useState(value); const { run } = useDebounceFn(() => { setDebounced(value); }, options); useEffect(() => { run(); }, [value]); return debounced; } ``` **useThrottle** 节流 ```scss import { useEffect, useState, useMemo } from 'react'; interface ThrottleOptions { wait?: number; leading?: boolean; trailing?: boolean; } function useThrottleFn(fn: T, options?: ThrottleOptions) { if (isDev) { if (typeof fn !== 'function') { console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`); } } const fnRef = useLatest(fn); const wait = options?.wait ?? 1000; const throttled = useMemo( () => throttle( (...args: Parameters): ReturnType => { return fnRef.current(...args); }, wait, options, ), [], ); useUnmount(() => { throttled.cancel(); }); return { run: throttled, cancel: throttled.cancel, flush: throttled.flush, }; } function useThrottle(value: T, options?: ThrottleOptions) { const [throttled, setThrottled] = useState(value); const { run } = useThrottleFn(() => { setThrottled(value); }, options); useEffect(() => { run(); }, [value]); return throttled; } ``` **useToggle** toggle在两个状态之间切换 ```typescript import { useState, useMemo } from 'react'; function useToggle(defaultValue: D = false as unknown as D, reverseValue?: R) { // 默认值,初始为false const [state, setState] = useState(defaultValue); const actions = useMemo(() => { // 默认值取反值 const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R; // 切换函数 const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue)); // 设置值函数 const set = (value: D | R) => setState(value); // 取默认值 const setLeft = () => setState(defaultValue); // 取反值 const setRight = () => setState(reverseValueOrigin); return { toggle, set, setLeft, setRight, }; }, []); return [state, actions]; } ``` **useCookieState** 将state存储在cookie中,作用就是刷新页面数据不丢失 ```ini import Cookies from 'js-cookie'; import { useState, useRef, useMemo } from 'react'; const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; function useMemoizedFn(fn: T) { if (isDev) { if (typeof fn !== 'function') { console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`); } } const fnRef = useRef(fn); fnRef.current = useMemo(() => fn, [fn]); const memoizedFn = useRef>(); if (!memoizedFn.current) { memoizedFn.current = function (this, ...args) { return fnRef.current.apply(this, args); }; } return memoizedFn.current as T; } function useCookieState(cookieKey: string, options: Options = {}) { const [state, setState] = useState(() => { // cookie根据key拿到值 const cookieValue = Cookies.get(cookieKey); if (typeof cookieValue === 'string') return cookieValue; if (typeof options.defaultValue === 'function') { return options.defaultValue(); } return options.defaultValue; }); const updateState = useMemoizedFn( ( newValue: State | ((prevState: State) => State), newOptions: Cookies.CookieAttributes = {}, ) => { const { defaultValue, ...restOptions } = { ...options, ...newOptions }; const value = typeof newValue === 'function' ? newValue(state) : newValue; setState(value); if (value === undefined) { Cookies.remove(cookieKey); } else { Cookies.set(cookieKey, value, restOptions); } }, ); return [state, updateState] as const; } ``` **useLocalStorageState** localStorage的hooks版本 ```ini const isFunction = (value: unknown): value is (...args: any) => any => typeof value === 'function'; const isUndef = (value: unknown): value is undefined => typeof value === 'undefined'; const isBrowser = !!( typeof window !== 'undefined' && window.document && window.document.createElement ); const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined)); function createUseStorageState(getStorage: () => Storage | undefined) { function useStorageState(key: string, options: Options = {}) { let storage: Storage | undefined; const { onError = (e) => { console.error(e); }, } = options; try { storage = getStorage(); } catch (err) { onError(err); } const serializer = (value: T) => { if (options.serializer) { return options.serializer(value); } return JSON.stringify(value); }; const deserializer = (value: string): T => { if (options.deserializer) { return options.deserializer(value); } return JSON.parse(value); }; function getStoredValue() { try { const raw = storage?.getItem(key); if (raw) { return deserializer(raw); } } catch (e) { onError(e); } if (isFunction(options.defaultValue)) { return options.defaultValue(); } return options.defaultValue; } const [state, setState] = useState(getStoredValue); useUpdateEffect(() => { setState(getStoredValue()); }, [key]); const updateState = (value?: SetState) => { const currentState = isFunction(value) ? value(state) : value; setState(currentState); if (isUndef(currentState)) { storage?.removeItem(key); } else { try { storage?.setItem(key, serializer(currentState)); } catch (e) { console.error(e); } } }; return [state, useMemoizedFn(updateState)] as const; } return useStorageState; } ``` **useSessionStorageState** sessionStoage的hooks版本 ```scss function createUseStorageState(getStorage: () => Storage | undefined) { function useStorageState(key: string, options: Options = {}) { let storage: Storage | undefined; const { onError = (e) => { console.error(e); }, } = options; try { storage = getStorage(); } catch (err) { onError(err); } const serializer = (value: T) => { if (options.serializer) { return options.serializer(value); } return JSON.stringify(value); }; const deserializer = (value: string): T => { if (options.deserializer) { return options.deserializer(value); } return JSON.parse(value); }; function getStoredValue() { try { const raw = storage?.getItem(key); if (raw) { return deserializer(raw); } } catch (e) { onError(e); } if (isFunction(options.defaultValue)) { return options.defaultValue(); } return options.defaultValue; } const [state, setState] = useState(getStoredValue); useUpdateEffect(() => { setState(getStoredValue()); }, [key]); const updateState = (value?: SetState) => { const currentState = isFunction(value) ? value(state) : value; setState(currentState); if (isUndef(currentState)) { storage?.removeItem(key); } else { try { storage?.setItem(key, serializer(currentState)); } catch (e) { console.error(e); } } }; return [state, useMemoizedFn(updateState)] as const; } return useStorageState; } const useSessionStorageState = createUseStorageState(() => isBrowser ? sessionStorage : undefined, ); ``` **useDrag \& useDrop** 处理拖拽 useDrag ```ini import { useRef, useEffect } from 'react'; const useEffectWithTarget = createEffectWithTarget(useEffect); const useDrag = (data: T, target: BasicTarget, options: Options = {}) => { // useLatest上面已经定义过了,引入即可 const optionsRef = useLatest(options); const dataRef = useLatest(data); const imageElementRef = useRef(); const { dragImage } = optionsRef.current; // useMount已定义过,引入即可 useMount(() => { if (dragImage?.image) { const { image } = dragImage; if (isString(image)) { const imageElement = new Image(); imageElement.src = image; imageElementRef.current = imageElement; } else { imageElementRef.current = image; } } }); useEffectWithTarget( () => { const targetElement = getTargetElement(target); if (!targetElement?.addEventListener) { return; } const onDragStart = (event: React.DragEvent) => { optionsRef.current.onDragStart?.(event); event.dataTransfer.setData('custom', JSON.stringify(dataRef.current)); if (dragImage?.image && imageElementRef.current) { const { offsetX = 0, offsetY = 0 } = dragImage; event.dataTransfer.setDragImage(imageElementRef.current, offsetX, offsetY); } }; const onDragEnd = (event: React.DragEvent) => { optionsRef.current.onDragEnd?.(event); }; targetElement.setAttribute('draggable', 'true'); targetElement.addEventListener('dragstart', onDragStart as any); targetElement.addEventListener('dragend', onDragEnd as any); return () => { targetElement.removeEventListener('dragstart', onDragStart as any); targetElement.removeEventListener('dragend', onDragEnd as any); }; }, [], target, ); }; ``` useDrop ```ini import { useRef } from 'react'; const useDrop = (target: BasicTarget, options: Options = {}) => { const optionsRef = useLatest(options); // https://stackoverflow.com/a/26459269 const dragEnterTarget = useRef(); useEffectWithTarget( () => { const targetElement = getTargetElement(target); if (!targetElement?.addEventListener) { return; } const onData = ( dataTransfer: DataTransfer, event: React.DragEvent | React.ClipboardEvent, ) => { const uri = dataTransfer.getData('text/uri-list'); const dom = dataTransfer.getData('custom'); 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; } if (uri && optionsRef.current.onUri) { optionsRef.current.onUri(uri, event as React.DragEvent); return; } if (dataTransfer.files && dataTransfer.files.length && optionsRef.current.onFiles) { optionsRef.current.onFiles(Array.from(dataTransfer.files), event as React.DragEvent); return; } if (dataTransfer.items && dataTransfer.items.length && optionsRef.current.onText) { dataTransfer.items[0].getAsString((text) => { optionsRef.current.onText!(text, event as React.ClipboardEvent); }); } }; const onDragEnter = (event: React.DragEvent) => { event.preventDefault(); event.stopPropagation(); dragEnterTarget.current = event.target; optionsRef.current.onDragEnter?.(event); }; const onDragOver = (event: React.DragEvent) => { event.preventDefault(); optionsRef.current.onDragOver?.(event); }; const onDragLeave = (event: React.DragEvent) => { if (event.target === dragEnterTarget.current) { optionsRef.current.onDragLeave?.(event); } }; const onDrop = (event: React.DragEvent) => { event.preventDefault(); onData(event.dataTransfer, event); optionsRef.current.onDrop?.(event); }; const onPaste = (event: React.ClipboardEvent) => { onData(event.clipboardData, event); optionsRef.current.onPaste?.(event); }; targetElement.addEventListener('dragenter', onDragEnter as any); targetElement.addEventListener('dragover', onDragOver as any); targetElement.addEventListener('dragleave', onDragLeave as any); targetElement.addEventListener('drop', onDrop as any); targetElement.addEventListener('paste', onPaste as any); return () => { targetElement.removeEventListener('dragenter', onDragEnter as any); targetElement.removeEventListener('dragover', onDragOver as any); targetElement.removeEventListener('dragleave', onDragLeave as any); targetElement.removeEventListener('drop', onDrop as any); targetElement.removeEventListener('paste', onPaste as any); }; }, [], target, ); }; ``` **useVirtualList** 提供虚拟列表能力 ```ini const useVirtualList = (list: T[], options: Options) => { const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options; const itemHeightRef = useLatest(itemHeight); const size = useSize(containerTarget); const scrollTriggerByScrollToFunc = useRef(false); const [targetList, setTargetList] = useState<{ index: number; data: T }[]>([]); const [wrapperStyle, setWrapperStyle] = useState({}); const getVisibleCount = (containerHeight: number, fromIndex: number) => { if (isNumber(itemHeightRef.current)) { return Math.ceil(containerHeight / itemHeightRef.current); } let sum = 0; let endIndex = 0; for (let i = fromIndex; i < list.length; i++) { const height = itemHeightRef.current(i, list[i]); sum += height; endIndex = i; if (sum >= containerHeight) { break; } } return endIndex - fromIndex; }; const getOffset = (scrollTop: number) => { if (isNumber(itemHeightRef.current)) { return Math.floor(scrollTop / itemHeightRef.current) + 1; } let sum = 0; let offset = 0; for (let i = 0; i < list.length; i++) { const height = itemHeightRef.current(i, list[i]); sum += height; if (sum >= scrollTop) { offset = i; break; } } return offset + 1; }; // 获取上部高度 const getDistanceTop = (index: number) => { if (isNumber(itemHeightRef.current)) { const height = index * itemHeightRef.current; return height; } const height = list .slice(0, index) .reduce((sum, _, i) => sum + (itemHeightRef.current as ItemHeight)(i, list[i]), 0); return height; }; const totalHeight = useMemo(() => { if (isNumber(itemHeightRef.current)) { return list.length * itemHeightRef.current; } return list.reduce( (sum, _, index) => sum + (itemHeightRef.current as ItemHeight)(index, list[index]), 0, ); }, [list]); const calculateRange = () => { const container = getTargetElement(containerTarget); if (container) { const { scrollTop, clientHeight } = container; const offset = getOffset(scrollTop); const visibleCount = getVisibleCount(clientHeight, offset); const start = Math.max(0, offset - overscan); const end = Math.min(list.length, offset + visibleCount + overscan); const offsetTop = getDistanceTop(start); setWrapperStyle({ height: totalHeight - offsetTop + 'px', marginTop: offsetTop + 'px', }); setTargetList( list.slice(start, end).map((ele, index) => ({ data: ele, index: index + start, })), ); } }; useUpdateEffect(() => { const wrapper = getTargetElement(wrapperTarget) as HTMLElement; if (wrapper) { Object.keys(wrapperStyle).forEach((key) => (wrapper.style[key] = wrapperStyle[key])); } }, [wrapperStyle]); useEffect(() => { if (!size?.width || !size?.height) { return; } calculateRange(); }, [size?.width, size?.height, list]); useEventListener( 'scroll', (e) => { if (scrollTriggerByScrollToFunc.current) { scrollTriggerByScrollToFunc.current = false; return; } e.preventDefault(); calculateRange(); }, { target: containerTarget, }, ); const scrollTo = (index: number) => { const container = getTargetElement(containerTarget); if (container) { scrollTriggerByScrollToFunc.current = true; container.scrollTop = getDistanceTop(index); calculateRange(); } }; return [targetList, useMemoizedFn(scrollTo)] as const; }; ``` 更多hooks请看参考文档第一条 ## vue hooks ### useRequest 我去掉了ts部分方便不会ts的同学理解,用法参考[dewfall123.github.io/ahooks-vue/...](https://link.juejin.cn?target=https%3A%2F%2Fdewfall123.github.io%2Fahooks-vue%2Fzh%2Fuse-request%2F%23basic-api "https://dewfall123.github.io/ahooks-vue/zh/use-request/#basic-api") ```ini function useRequest(service,options) { let contextConfig = {}; if (getCurrentInstance()) { contextConfig = inject(RequestConfig, {}); } // options选项合并 const finalOptions = { ...DefaultOptions, ...contextConfig, ...options }; const { // 请求方法 requestMethod, // loading状态 defaultLoading, // boolean值,manual为true时需要手动调用run函数才会触发请求 manual, throwOnError, onSuccess, onError, onFinally, formatResult, initialData, // 默认参数 defaultParams, // 可以延迟 `loading` 变成 `true` 的时间,有效防止闪烁 loadingDelay, debounceInterval, // 设置为false时,loading 不会在第一时间变成`true`,要等到`debounceInterval`ms 后,即函数真正执行时 loadingWhenDebounceStart, // 节流模式,单位毫秒 throttleInterval, // 轮询间隔,单位毫秒 pollingInterval, // pollingWhenHidden设置为false时,在屏幕不可见时,暂时暂停定时任务 pollingWhenHidden, // pollingSinceLastFinished设置为false时,每隔`pollingInterval`ms 都会执行一次请求,而不是等上次请求结束 pollingSinceLastFinished, // 在屏幕重新获取焦点或重新显示时,是否重新发起请求。默认为 `false`,即不会重新发起请求。 refreshOnWindowFocus, // watch 到 refreshDeps 变化,会触发 service 重新执行 refreshDeps, ready, } = finalOptions; let promiseService: (...args) => Promise; if (['string', 'object'].includes(service)) { promiseService = () => requestMethod(service); } else { promiseService = (...args) => new Promise((resolve, reject) => { const returnedService = service(...args); let fn = returnedService; if (! returnedService.then) { if (!['string', 'object'].includes(returnedService)) { throw new Error( 'If sevice is a function, it must return a String, Object or Promise', ); } fn = requestMethod(returnedService); } fn.then(resolve).catch(reject); }); } const loading = ref(defaultLoading); const data = ref(initialData); const error = ref(); const params = ref(defaultParams); const lastSuccessParams = ref(defaultParams); let count = 0; let unmountedFlag = false; if (getCurrentInstance()) { onUnmounted(() => { unmountedFlag = true; }); } let isVisible = ref(true); if (getCurrentInstance()) { isVisible = useDocumentVisibility({ // 页面聚焦时请求一次 onVisible() { if (refreshOnWindowFocus) { refresh(); } }, }).isVisible; } let loadingDelayTimer: any; let pollingSinceFinishedTimer: any; function _run(...args) { // 只要 ready=false 不执行 if (!ready.value) { return Promise.resolve(); } if (pollingSinceFinishedTimer) { clearTimeout(pollingSinceFinishedTimer); } if (loadingDelayTimer) { clearTimeout(loadingDelayTimer); } // 设置loading if (loadingDelay) { loadingDelayTimer = setTimeout(() => { loading.value = true; }, loadingDelay); } else { loading.value = true; } count++; const curCount = count; params.value = cloneDeep(args); // 抛弃该次请求结果 const shouldAbandon = () => unmountedFlag || curCount !== count; return promiseService(...args) .then(res => { if (shouldAbandon()) { return; } const formattedResult = formatResult(res); data.value = formattedResult; // fix #21 error.value = undefined; lastSuccessParams.value = cloneDeep(args); onSuccess(formattedResult, args); return formattedResult; }) .catch(err => { if (shouldAbandon()) { return; } console.error(err); error.value = err; onError(err, args); if (throwOnError) { throw err; } }) .finally(() => { if (shouldAbandon()) { return; } if (loadingDelayTimer) { clearTimeout(loadingDelayTimer); } // 在请求结束时轮询 if (pollingInterval && pollingSinceLastFinished) { // 当时页面不可见,等页面可见再查询 if (pollingWhenHidden && !isVisible.value) { pollingSinceFinishedTimer = setInterval(() => { // 需要恢复查询 if (!(pollingWhenHidden && !isVisible.value)) { clearInterval(pollingSinceFinishedTimer); _run(...args); } }, pollingInterval); } else { pollingSinceFinishedTimer = setTimeout(() => { _run(...args); }, pollingInterval); } } loading.value = false; onFinally(); }); } const cancel = () => { if (pollingTimer) { clearInterval(pollingTimer); } if (pollingSinceFinishedTimer) { clearTimeout(pollingSinceFinishedTimer); } if (loadingDelayTimer) { clearTimeout(loadingDelayTimer); } count++; loading.value = false; }; let run = _run; if (debounceInterval) { const debounceRun = debounce(_run, debounceInterval); run = (...args) => { // 在debounce等待阶段把loading设置为true,比loadingDelay优先级高 if (loadingWhenDebounceStart) { loading.value = true; } return Promise.resolve(debounceRun(...args)!); }; } if (throttleInterval) { const throttleRun = throttle(_run, throttleInterval); run = (...args) => { return Promise.resolve(throttleRun(...args)!); }; } let pollingTimer: any; // 每隔x时间开始轮询,不管上次什么时候结束 if (pollingInterval && !pollingSinceLastFinished) { run = (...args) => { if (pollingTimer) { clearInterval(pollingTimer); } pollingTimer = setInterval(() => { if (pollingWhenHidden && !isVisible.value) { return; } _run(...args); }, pollingInterval); return _run(...args); }; } function refresh() { return run(...params.value); } // 自动执行 if (!manual) { // ready 变为true 自动发起请求,会带上参数 options.defaultParams watch( ready, () => { if (ready.value) { run(...defaultParams); } }, { immediate: true, }, ); } // refreshDeps watch(refreshDeps, refresh); return { // 是否正在加载 loading, // 抛出异常 error, // 返回的数据,默认为undefined data, // 手动触发 run, // 参数数组 params, // 只有执行成功才赋值的参数 lastSuccessParams, // 取消当前请求,如果有轮询就停止 cancel, // 使用上一次的params重新执行service refresh, }; } ``` ### useAxios 用于axios请求的hook,基于useRequest封装 ```csharp function useAxios(params,options) { return useRequest(params, { ...options, requestMethod, }); } ``` ### useToggle 用于在两个状态值间切换的 Hook 上面react已经有ts版本的了,vue就用js版本的 ```ini function useToggle(defaultValue,reverseValue) { const state = ref(defaultValue); const setState = (value) => { state.value = value; }; const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue); // 切换返回值 const toggle = (value) => { if (value === undefined) { value = state.value === defaultValue ? reverseValueOrigin : defaultValue; } setState(value); }; // 设置默认值 const setLeft = () => { setState(defaultValue); }; // 设置取反值 const setRight = () => { setState(reverseValueOrigin); }; return { state, toggle, setLeft, setRight, }; } ``` ### useBoolean 跟上面useToggle的区别就是只接受传入boolean值 ```ini const useBoolean = (defaultValue = false) => { const { state, toggle } = useToggle(defaultValue); const setTrue = () => toggle(true); const setFalse = () => toggle(false); const toggleBoolean = () => toggle(); return { state, toggle: toggleBoolean, setTrue, setFalse, }; }; ``` ### useHover 一个用于追踪 dom 元素是否有鼠标悬停的 Hook 通过监听鼠标移入移出事件mouseenter/mouseleave实现 ```scss function useHover(target, options) { const { state, setFalse, setTrue } = useBoolean(false); const { onEnter, onLeave } = options ?? {}; function onMouseEnter() { setTrue(); onEnter && onEnter(); } function onMouseLeave() { setFalse && setFalse(); onLeave && onLeave(); } safeOnMounted(() => { const targetElement = getTargetElement(target); if (!targetElement) { return; } targetElement.addEventListener('mouseenter', onMouseEnter); targetElement.addEventListener('mouseleave', onMouseLeave); }); onUnmounted(() => { // 等价于document.querySelector const targetElement = getTargetElement(target); if (targetElement) { targetElement.removeEventListener('mouseenter', onMouseEnter); targetElement.removeEventListener('mouseleave', onMouseLeave); } }); return state; } ``` ### useKeyPress 用于管理键盘事件的hook,比如键盘点击enter键,点击shift键等 ```ini const defaultEvents = ['keydown']; function useKeyPress( keyFilter, eventHandler, option, ) { const { events = defaultEvents, target } = option; const isKeyEvent = genKeyFormater(keyFilter); safeOnMounted(() => { const handlers = []; // 类似于document.getElementById,区别是获取不到target时默认值为window const el = getTargetElement(target, window)!; for (const eventName of events) { const handler = (event) => { if (isKeyEvent(event)) { return eventHandler(event); } }; handlers.push(handler); el.addEventListener(eventName, handler); } onUnmounted(() => { for (const eventName of events) { const handler = handlers.shift()!; el.removeEventListener(eventName, handler); } }); }); } /** * 键盘输入预处理方法 * @param [keyFilter] 当前键 * @returns () => Boolean */ export function genKeyFormater(keyFilter) { const type = isType(keyFilter); if (type === 'function') { return keyFilter; } if (type === 'string' || type === 'number') { return (event) => genFilterKey(event, keyFilter); } if (type === 'array') { return (event) => keyFilter.some((item) => genFilterKey(event, item)); } return keyFilter ? () => true : () => false; } ``` **useTable** 封装了表格常用的功能的hook,支持分页、排序、搜索 ```ini function useTable(data,options){ const { page, sort, search } = merge( {}, defaultParams, options, ); const dataRef = isRef(data) ? data : (computed(() => data); const page = ref(pageParams); const sort = ref(sortParams); const search = ref(searchParams); const throlltedSearchText = useThrottle(() => search.value.text, { wait: 500, }); const filtedList = computed(() => { let list = [...dataRef.value]; // 查询 const searchText = throlltedSearchText.value.trim(); if (searchText) { list = list.filter(i => { let iStr = ''; if (search.value.keys?.length) { iStr = search.value.keys.map(key => i[key]).join(' '); } else { iStr = JSON.stringify(i); } if (search.value.isReg) { try { const matched = Boolean(iStr.match(new RegExp(searchText))); return matched; } catch { } } return iStr .toLocaleLowerCase() .includes(searchText.toLocaleLowerCase()); }); } // 排序处理 if (sort.value.key) { list.sort((a, b) => { const key = sort.value.key; const sortResult = sort.value.compareFn( a, b, ); return sort.value.direction === 'ascend' ? sortResult : sortResult * -1; }); } return list; }); const total = computed(() => filtedList.value.length); const pagedData = computed(() => { let list = [...filtedList.value]; // page slice const start = (page.value.index - 1) * page.value.size; const end = start + page.value.size; list = list.slice(start, end); return list; }); watch([data], () => { search.value.text = ''; page.value.index = 1; }); watch( () => page.value.size, () => { page.value.index = 1; }, { immediate: false, }, ); watch( () => filtedList.value, () => { page.value.index = 1; }, ); return { data, page, search, sort, pagedData, total, }; } ``` ## 往期文章 [🥇🥇🥇 一网打尽主流的微前端框架生命周期/路由/资源系统:原理篇](https://juejin.cn/post/7311907901047324722 "https://juejin.cn/post/7311907901047324722") [一文带你梳理React面试题(2023年版本)](https://juejin.cn/post/7182382408807743548 "https://juejin.cn/post/7182382408807743548") [抽丝剥茧带你复习vue源码(2023年面试版本)](https://juejin.cn/post/7195517440344211512 "https://juejin.cn/post/7195517440344211512") [🥇🥇🥇一文带你打通微前端-qiankun/microapp/icestark/wujie全解析](https://juejin.cn/post/7308583491934994470 "https://juejin.cn/post/7308583491934994470") [🔥🔥🔥手把手带你集成低代码平台应用](https://juejin.cn/post/7280001048429789240 "https://juejin.cn/post/7280001048429789240") [🛬🛬🛬手把手带你集成阿里低代码引擎vue物料库](https://juejin.cn/post/7249586143011962936 "https://juejin.cn/post/7249586143011962936") [🐒🐒🐒猴子都能看懂的Rxjs教程(上篇)](https://juejin.cn/post/7298904569057771546 "https://juejin.cn/post/7298904569057771546") [🐟🐟🐟摸鱼时间到!两分钟实现掘金文字版权效果](https://juejin.cn/post/7301901927813906483 "https://juejin.cn/post/7301901927813906483") [项目实战:跨项目组件复用方案选择](https://juejin.cn/post/7163521062926286855 "https://juejin.cn/post/7163521062926286855") [5k字长文总结性能优化:构建你的前端知识体系](https://juejin.cn/post/7129471837947297828 "https://juejin.cn/post/7129471837947297828") ## 参考文档 1. [hooks Github](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Falibaba%2Fhooks "https://github.com/alibaba/hooks") 2. [hooks-vue Github](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fdewfall123%2Fahooks-vue "https://github.com/dewfall123/ahooks-vue") 3. [react-use](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fstreamich%2Freact-use "https://github.com/streamich/react-use")

相关推荐
zyk_5202 分钟前
前端渲染pdf文件解决方案-pdf.js
前端·javascript·pdf
Apifox.9 分钟前
Apifox 4月更新|Apifox在线文档支持LLMs.txt、评论支持使用@提及成员、支持为团队配置「IP 允许访问名单」
前端·人工智能·后端·ai·ai编程
划水不带桨15 分钟前
大数据去重
前端
沉迷...20 分钟前
手动实现legend 与 echarts图交互 通过js事件实现图标某项的高亮 显示与隐藏
前端·javascript·echarts
可观测性用观测云35 分钟前
观测云数据在Grafana展示的最佳实践
前端
uwvwko1 小时前
ctfhow——web入门214~218(时间盲注开始)
前端·数据库·mysql·ctf
Json____1 小时前
使用vue2开发一个医疗预约挂号平台-前端静态网站项目练习
前端·vue2·网站模板·静态网站·项目练习·挂号系统
HuaHua的世界1 小时前
说说 Vue 中 CSS scoped 的原理?
css·vue.js
littleplayer1 小时前
iOS Swift Redux 架构详解
前端·设计模式·架构
工呈士1 小时前
HTML 模板技术与服务端渲染
前端·html