封装常用的工具函数助力业务开发(含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")

相关推荐
腾讯TNTWeb前端团队11 分钟前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom5 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom5 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom5 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom5 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom6 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试