前端人 精学ahooks源码
背景
特别喜欢红宝书的一句话“站在巨人的肩上”,起因是觉得react中的hooks语法,用了那么长时间,有的时候还是似懂非懂,所以才有了这一篇文章,学习一下前人封装hook的方法。沉浸式学习,从浅入深,一点一点吃透,也希望大家能够从 理解思想 -> 输出思想,这样社区才能越来越好,帮助自己也帮助大家。如果文章有错误,也希望各位指点一二。 本文将持续迭代,赶紧收藏起来吧,
收藏===学会
~
前置知识
前言
- 如果
react hooks
语法不熟练,请确保看过一遍官方文档,为了让代码简洁,react hooks
将不再引入,当它存在即可 - 文章会直接跳过一些边界情况,
isFunc
,isBroswer
等,如有遗漏这些工具函数判断,自行判断下,以及大部分Ts
类型,单元测试等。把更多时间专注于hooks
的封装,避免造成一些心智负担 - 一些辅助函数
useLatest
,useMemoizedFn
,useUpdateEffect
等,就不再引入,默认当它存在即可 - 保留了大部分
ahooks
源码,但是我更改了一部分,我觉得更改了一部分源码,才知道你学习的ahooks
源码,这是我有意为之 - 最佳学习路线,照着ahooks官网,一遍看案例,一边对照源码学习,
有疑惑的地方先思考,再动手,动手,一定要动手
状态就像快照
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1) // count: 0
}, 1000)
return () => clearInterval(timer)
}, [])
- 执行过程:
- 页面挂载后,开启定时器,1s以后,执行回调函数,此时的count为0,递增它
- 2s以后,再次回调计时器函数,此时的count还是0,状态就好比快照(闭包问题,effect中的回调函数只会执行一次,拿到的初始化的值)
- 无论过了多久,回调中的count永远是0
- 当组件卸载后,清除定时器
- 传送门:
获取最新的值
- useLatest 返回当前最新的值
function useLatest<T>(value: T) {
const ref = useRef(value)
ref.current = value
return ref
}
- 解决闭包问题
const [count, setCount] = useState(0)
const latestCount = useLatest(count)
useEffect(() => {
const timer = setInterval(() => {
setCount(latestCount.current + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
- 代码解析
- latestCount每次可以拿到最新的值,其本质就是利用useRef
- 组件初始化时,useRef的初始值为count(即0),后续渲染获取最新的count进行赋值
辅助函数
useMemoizedFn
- 用于代替
useCallback
,当它的deps
依赖发生变更,它返回的函数地址会变化,而useMemoizedFn
不会
const useMemoizedFn = (fn) => {
const fnRef = useRef(fn)
fnRef.current = useMemo(() => fn, [fn])
const memoizedFn = useRef()
if (!memoizedFn.current) {
memoizedFn.current = function (...args) {
// return fn(...args)
return fnRef.current(...args)
}
}
return memoizedFn.current
}
- 执行过程:
- 当使用该
hook
的组件,组件重新执行,意味着fn
回调重新定义 - 保存该
fn
函数,每次拿到最新的fn
,避免闭包问题,不使用fnRef
保存最新的函数引用,那么意味着,fn
中拿到的state
不会是最新的,即(return fn(...args))
- 本质上是通过
memoizedFn
去执行fnRef
函数,返回的是一个ref
,所以保证函数地址永远不会变化。
- 当使用该
useUpdateEffect / useUpdateLayoutEffect
- 忽略首次执行,只有依赖发生改变才会执行
const useUpdateEffect = createUpdateEffect(useEffect)
const useUpdateLayoutEffect = createUpdateEffect(useLayoutEffect)
function createUpdateEffect(hook) {
return (effect, deps) => {
const isMounted = useRef(false)
// 热更新 重置
hook(
() => () => {
isMounted.current = false
},
[]
)
hook(() => {
if (!isMounted.current) {
isMounted.current = true
} else {
return effect()
}
}, deps)
}
}
- 就定义一个ref状态即可,
isMounted
控制是否已经挂载
LifeCycle
useMount
- 组件初始化时执行
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.()
}, [])
}
useUnmount
- 组件卸载时执行
const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn)
useEffect(
() => () => {
fnRef.current()
},
[]
)
}
useUnmountedRef
- 当前组件是否已经卸载
const useUnmountedRef = () => {
const unmountedRef = useRef(false)
useEffect(() => {
unmountedRef.current = false
return () => {
unmountedRef.current = true
}
}, [])
return unmountedRef
}
State
useSetState
- 案例说明
const [state, setState] = useSetState({
name: 'ice',
age: 24,
})
<div>
<p>{JSON.stringify(state)}</p>
<button onClick={() => setState({ age: ++state.age })}>age:+1</button>
</div>
)
- 源码剖析
- 可用于合并对象,基本与
class
中的this.setState
一致 useCallback
用于性能优化,如果组件更新重新执行函数,则setMergeState
重新定义(引用不同,如果传递给子组件,导致子组件进行不必要的执行)
- 可用于合并对象,基本与
const isFunc = (val) => typeof val === 'function'
export const useSetState = (initialState) => {
const [state, setState] = useState(initialState)
const setMergeState = useCallback((patch) => {
setState((prevState) => {
const newState = isFunc(patch) ? patch(prevState) : patch
return newState ? { ...prevState, ...newState } : prevState
})
}, [])
return [state, setMergeState]
}
useToggle
const useToggle = (defaultVal = false, reverseValue) => {
const [state, setState] = useState(defaultVal)
const actions = useMemo(() => {
const reverseValueOrigin = reverseValue === undefined ? !defaultVal : reverseValue
const toggle = () => setState((s) => (s === reverseValueOrigin ? defaultVal : reverseValueOrigin))
const set = (v) => setState(v)
const setLeft = () => setState(defaultVal)
const setRight = () => setState(reverseValueOrigin)
return { toggle, set, setLeft, setRight }
}, [])
return [state, actions]
}
useMemo
用于性能优化,使用该hook的组件重新渲染,actions能够缓存计算结果setState
回调函数的写法,则能拿到上一次的值- 传送门:react.docschina.org/reference/r…
useBoolean
const useBoolean = (defaultVal: boolean = false) => {
const [state, { toggle, set }] = useToggle(!!defaultVal)
const actions = useMemo(() => {
const setTrue = () => set(true)
const setFalse = () => set(false)
return {
toggle,
set: (v) => set(!!v),
setTrue,
setFalse,
}
}, [])
return [state, actions]
}
- 本质就是调用
useToggle
useLocalStorageState / useSessionStorageState
export type SetState<S> = S | ((prevState?: S) => S)
export interface Options<T> {
defaultValue?: T | (() => T)
serializer?: (value: T) => string
deserializer?: (value: string) => T
onError?: (error: unknown) => void
}
const isBroswer = true
const isFunc = (v: unknown): v is (...args: any) => any => typeof v === 'function'
export const useLocalStorageState = createLocalStorageState(() => (isBroswer ? localStorage : undefined))
export const useSessionStorageState = createLocalStorageState(() => (isBroswer ? sessionStorage : undefined))
function createLocalStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined
const {
onError = (e) => {
console.error(e)
},
} = options
try {
storage = getStorage()
} catch (e) {
onError(e)
}
// 序列化
const serializer = (value: T) => {
if (options.serializer) {
return options.serializer(value)
}
return JSON.stringify(value)
}
// 反序列化
const deserializer = (value: string) => {
if (options.deserializer) {
return options.deserializer(value)
}
return JSON.parse(value)
}
function getStorageValue() {
const raw = storage?.getItem(key)
if (raw) {
return deserializer(raw)
}
if (isFunc(options.defaultValue)) {
return options.defaultValue()
}
return options.defaultValue
}
function updateState(value?: SetState<T>) {
const currentState = isFunc(value) ? value(state) : value
setState(currentState)
if (currentState === undefined) {
storage?.removeItem(key)
} else {
storage?.setItem(key, serializer(currentState))
}
}
// 当key修改后,重新获取值
useUpdateEffect(() => {
setState(getStorageValue())
}, [key])
const [state, setState] = useState(getStorageValue)
return [state, useMemoizedFn(updateState)]
}
return useStorageState
}
- 核心函数
getStorageValue
- 先获取,有的话用本地值,没有用默认值
updateState
- 更新最新的值,如果为undefined移除当前缓存值
serializer
- 序列化,有传入的优先使用传入的,没有直接序列化
deserializer
- 反序列化,有传入的优先使用传入的,没有直接反序列化
useDebounce / useDebounceFn
import { debounce } from 'lodash-es'
function useDebounce(value, options) {
const [debounced, setDebounced] = useState(value)
const { run } = useDebounceFn(() => {
setDebounced(value)
}, options)
useEffect(() => {
run()
}, [value])
return debounced
}
function useDebounceFn(fn, options) {
const fnRef = useLatest(fn)
const wait = options?.wait ?? 1000
const debounced = useMemo(
() =>
debounce(
(...args) => {
fnRef.current(...args)
},
wait,
options
),
[]
)
const { cancel, flush } = debounced
useUnmount(() => {
cancel()
})
return { run: debounced, cancel, flush }
}
- useDebounce
debounced
是被防抖以后的值,只要在合适的时机调用setDebounced
即可,页面进行挂载或原始值value
更新,就执行run
函数,而run
函数,是useDebounceFn
的返回值
useDebounceFn
- 第一个参数
fn
(即回调函数),拿到最新的fn
函数(因为被useMemo
包裹) debounced
本质上就是debounce
函数的返回值,而debounce
函数,来自lodash
- 当页面卸载的时候调用
cancel
,避免内存泄漏
- 第一个参数
useThrottle / useThrottleFn
- 实现思路和 useDebounce / useDebounceFn 一致
useMap
const useMap = (initialValue) => {
const getInitialValue = () => new Map(initialValue)
const [map, setMap] = useState(getInitialValue)
const set = (key, val) => {
setMap((prev) => {
const temp = new Map(prev)
temp.set(key, val)
return temp
})
}
const setAll = (newMap) => {
setMap(new Map(newMap))
}
const remove = (key) => {
setMap((prev) => {
const temp = new Map(prev)
temp.delete(key)
return temp
})
}
const reset = () => setMap(getInitialValue())
const get = (key) => map.get(key)
return [
map,
{
set: useMemoizedFn(set),
setAll: useMemoizedFn(setAll),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
get: useMemoizedFn(get),
},
]
}
- 引用类型,需要视图更新,需要浅拷贝(即赋值一个新的
map
即可),因为setState
本质上会进行浅比较,(即前一个值和当前值比较Object.is(prev, cur)
)
useSet
function useSet(initialValue) {
const getInitialValue = () => new Set(initialValue)
const [set, setSet] = useState(getInitialValue)
const add = (key) => {
if (set.has(key)) {
return
}
setSet((prev) => {
const temp = new Set(prev)
temp.add(key)
return temp
})
}
const remove = (key) => {
if (!set.has(key)) {
return
}
setSet((prev) => {
const temp = new Set(prev)
temp.delete(key)
return temp
})
}
const reset = () => setSet(getInitialValue())
return [
set,
{
add: useMemoizedFn(add),
remove: useMemoizedFn(remove),
reset: useMemoizedFn(reset),
},
]
}
- 与
useMap
的思想基本一致,add
添加元素,如果存在直接return
,减少没必要的重新渲染,remove
同理,如果不存在,减少没必要的渲染
usePrevious
const defaultShouldUpdate = (a, b) => !Object.is(a, b)
function usePrevious(state, shouldUpdate = defaultShouldUpdate) {
const curRef = useRef(state)
const prevRef = useRef()
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current
curRef.current = state
}
return prevRef.current
}
- 双指针,利用两个
ref
,保存前一个值和当前值即可
useSafeState
function useSafeState(initialState) {
const unmountedRef = useUnmountedRef()
const [state, setState] = useState(initialState)
const setSafeState = useCallback((nextState) => {
if (unmountedRef.current) return
setState(nextState)
}, [])
return [state, setSafeState]
}
- 防止组件卸载后,异步回调引起的内存泄漏,判断当前组件是否已经卸载即可
useGetState
function useGetState(initialState) {
const [state, setState] = useState(initialState)
const latestRef = useLatest(state)
const getState = useCallback(() => latestRef.current, [])
return [state, setState, getState]
}
useResetState
function useResetState(initialState) {
const [state, setState] = useState(initialState)
const resetState = useCallback(() => {
setState(initialState)
}, [])
return [state, setState, resetState]
}
- 利用
useCallback
, 每次当hook
重新执行,而resetState
中的回调函数拿到的还是初始化的值,利用闭包
Advanced
useCreation
class Foo {
constructor() {
this.data = Math.random()
}
data: number
}
// 1. 当所在的函数式组件,重新执行的时候,Foo类也重新执行
const foo1 = useRef(new Foo())
// 2. 而它Foo只会执行一次
const foo2 = useCreation(() => new Foo(), [])))
- 代替
useRef
或者useMemo
, 因为useMemo
不一定会被重计算,而useRef
在创建一些复杂对象的时候,可能会存在潜在的性能问题
const depsAreSame = (oldDeps: DependencyList, deps: DependencyList) => {
if (oldDeps === deps) return true
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
export function useCreation<T>(factory: () => T, deps: DependencyList) {
const { current } = useRef({
deps,
obj: undefined as T | undefined,
initialized: false,
})
if (!current.initialized || !depsAreSame(current.deps, deps)) {
current.deps = deps
current.obj = factory()
current.initialized = true
}
return current.obj as T
}
- 只有两种情况下会重新执行
factory
- 初始化的时候
- 当依赖更新,依赖不相等的时候
useReactive
前置知识
- WeakMap
let obj = { name: 'ice', age: '20' } const m = new WeakMap() // Map则回阻止obj的回收 m.set(obj, 'xxx') const clearup = new FinalizationRegistry((key) => { console.log(key, '被垃圾回收了') }) clearup.register(obj, 'obj') obj = null
- 在
useReactive
中使用到了WeakMap
,若某个obj
被它引用为key
,当这个obj
被赋值为null
时,它不会阻止垃圾回收该对象(即弱引用),则Map
是强引用
- 在
- Proxy
vue3
中采用Proxy
,监听代理对象的变化,从而引起视图的变化
const obj = { name: 'ice', age: 24, get fullname() { console.log(this) return this.name + this.age }, } const proxy = new Proxy(obj, { get(target, key, receiver) { console.log('get 触发') console.log(target === obj) // true console.log(key) console.log(receiver === proxy) // true // return target[key] return Reflect.get(target, key, receiver) }, })
- 其中
Reflect
的作用进行反射,改变访问对象触发getter
时的this
指向,不然this
指向的则是obj
对象
源码实现
- 不过度考虑边缘情况
// 回调该函数,引起视图的变化
function useUpdate() {
const [, setState] = useState({})
return useCallback(() => {
setState({})
}, [])
}
const proxyMap = new WeakMap() // [k:v] [proxy:rawObj]
// 返回proxy对象,监听该对象的变化,执行回调函数
function observer(initialVal, cb: () => void) {
const existsingProxy = proxyMap.get(initialVal)
// 如果存在proxy,直接使用存在的
if (existsingProxy) {
return existsingProxy
}
const proxy = new Proxy(initialVal, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
return typeof res === 'object' ? observer(res, cb) : res
},
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver)
cb()
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
cb()
return res
},
})
proxyMap.set(initialVal, proxy)
return proxy
}
function useReactive(initialVal) {
const update = useUpdate()
const stateRef = useRef(initialVal)
return useMemo(() => observer(stateRef.current, update), [])
}
- 核心实现
- 返回一个代理对象,即(
proxy
)对象 - 定义
proxy
捕获器,get set deleteProperty
- 调用
update
, 触发页面更新
- 返回一个代理对象,即(
Effect
useAsyncEffect
// error
useEffect(async () => {
const res = await getName()
setName(res)
}, [])
// success
useAsyncEffect(async () => {
const res = await getName()
setName(res)
}, [])
- 支持异步函数,因为
useEffect
中的effect
不能传入异步函数
const isAsyncGenerator = (val) => val[Symbol.asyncIterator]
function useAsyncEffect(effect, deps) {
useEffect(() => {
const e = effect()
let cancelled = false
async function execute() {
if (isAsyncGenerator(e)) {
while (true) {
const res = await e.next()
if (res.done || cancelled) {
break
}
}
} else {
await e
}
}
execute()
return () => {
cancelled = true
}
}, deps)
}
- 分为以下两种情况
- 异步函数直接调用
- 异步生成器函数,不断的调用
next
,直至done
, 或者当依赖更新,执行effect[clearup]
函数,修改cancelled
循环结束
useDebounceEffect
function useDebounceEffect(effect: EffectCallback, deps?: DependencyList, options?: DebounceOptions) {
const [flag, setFlag] = useState({})
const { run } = useDebounceFn(() => {
setFlag({})
}, options)
useEffect(() => {
run()
}, deps)
useUpdateEffect(effect, [flag])
}
- 本质上对
effect
做了转发,当flag
改变时,引起视图的更新 - 当
deps
的依赖更新,触发useEffect
的回调函数执行,而此时的run
函数是一个被防抖过的函数
useThrottleEffect
function useThrottleEffect(effect: EffectCallback, deps?: DependencyList, options?: ThrottleOptions) {
const [flag, setFlag] = useState({})
const { run } = useThrottleFn(() => {
setFlag({})
}, options)
useEffect(() => {
run()
}, deps)
useUpdateEffect(effect, [flag])
}
- 逻辑跟
useDebounceEffect
一致
useDeepCompareEffect / useDeepCompareLayoutEffect
const useDeepCompareEffect = createDeepCompareEffect(useEffect)
const useDeepCompareLayoutEffect = createDeepCompareEffect(useLayoutEffect)
const createDeepCompareEffect = (hook) => (effect, deps) => {
const signalRef = useRef<number>(0)
const previous = usePrevious(deps)
if (deps === undefined || !isEqual(deps, previous)) {
signalRef.current++
}
hook(effect, [signalRef.current])
}
- 增加一个
signalRef
控制在合适的时机调用effect
即可,其中isEqual
是调用lodash的,判断两者是否深度相等
useInterval / useTimeout
const isNumber = (val: unknown): val is number => typeof val === 'number'
const useInterval = (fn: () => void, delay?: number, options: { immediate?: boolean } = {}) => {
const timerCallback = useMemoizedFn(fn)
const timerRef = useRef<number | null>(null)
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current) // clearTimeout
}
}, [])
useEffect(() => {
if (!isNumber(delay) || delay < 0) {
return
}
if (options.immediate) {
timerCallback()
}
timerRef.current = setInterval(timerCallback, delay) // setTimeout
return clear
}, [delay, options.immediate])
return clear
}
- 执行顺序
- 页面挂载前,
fn
函数被useMemoizedFn
包裹(当FC
重新执行,该timerCallback
引用从始至终都是同一个) - 进行一些变量定义
- 把清除定时器的
clear
函数返回 - 页面挂载后,执行
useEffect
,进行一些边界处理并且开启定时器,当delay/immediate
改变后,重新执行FC
- 页面挂载前,
useUpdate
- useReactive中实现过
useLockFn
function useLockFn<P extends any[] = any[], V = any>(fn: (...args: P) => Promise<V>) {
const lockRef = useRef(false)
return useCallback(
async (...args: P) => {
if (lockRef.current) return
lockRef.current = true
try {
const ret = await fn(...args)
return ret
} finally {
lockRef.current = false
}
},
[fn]
)
}
- 使用场景
- 可以用来代替部分防抖场景
- 其本质就是通过一个
ref
变量,控制fn
是否执行即可
Dom
dom类的设计到许多的工具函数,忽略一些边缘情况,下面一起来看下吧~
工具函数
getTargetElement
function getTargetElement(target) {
let targetElement
if (isFunc(target)) {
targetElement = target()
} else if ('current' in target) {
targetElement = target.current
} else {
targetElement = target
}
return targetElement
}
depsAreSame
const depsAreSame = (oldDeps: DependencyList, deps: DependencyList) => {
if (oldDeps === deps) return true
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], deps[i])) return false
}
return true
}
- 判断依赖是否相同,分为两种情况
- 同一个引用
- 遍历新老
deps
,Object.is
判断是否相同
useEventListener
useEventListener
的执行顺序稍微有点绕,我们来看下流程
useEventListener
useEffectWithTarget
createEffectWithTarget
returnValue
useEventListener
内部本质调用useEffectWithTarget
useEffectWithTarget
则是createEffectWithTarget
(利用闭包封装useEffect/useLayoutEffect
)- 最后的入参都会被
useEffectWithTarget
接收到,也就是createEffectWithTarget
的返回值
// useEffectWithTarget.ts
const useEffectWithTarget = createEffectWithTarget(useEffect)
// createEffectWithTarget.ts
export const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
const useEffectWithTarget = (effect: EffectCallback, deps: DependencyList, target) => {
const hasInitRef = useRef<boolean>(false)
const lastElementRef = useRef<Element[]>([])
const lastDepsRef = useRef<DependencyList>([])
const unloadRef = useRef<any>()
useEffectType(() => {
const targets = isArray(target) ? target : [target]
const els = targets.map((el) => getTargetElement(el))
if (!hasInitRef.current) {
hasInitRef.current = true
lastElementRef.current = els
lastDepsRef.current = deps
unloadRef.current = effect()
}
if (els.length !== lastElementRef.current.length || !depsAreSame(lastDepsRef.current, deps) || !depsAreSame(lastElementRef.current, els)) {
unloadRef.current?.()
lastElementRef.current = els
lastDepsRef.current = deps
unloadRef.current = effect()
}
})
useUnmount(() => {
unloadRef.current?.()
hasInitRef.current = false
})
}
return useEffectWithTarget
}
function useEventListener(eventName: string, handler: noop, options: Options = {}) {
const handlerRef = useLatest(handler)
useEffectWithTarget(
() => {
const el = getTargetElement(options.target)
if (!el?.addEventListener) return
const eventListener = (e) => {
handlerRef.current(e)
}
el.addEventListener(eventName, eventListener, {
capture: options.capture,
once: options.once,
passive: options.passive,
})
return () => {
el.removeEventListener(eventName, eventListener, {
capture: options.capture,
})
}
},
[eventName, options.once, options.passive],
options.target
)
}
createEffectWithTarget
- 其本质,就是控制
effect
在合适的时机执行- 初始化执行一次
els
长度不一致、els
发生改变或deps
发生改变执行
useEventListener -> useEffectWithTarget
- 只是监听/移除事件
useDocumentVisibility
type VisibilityState = 'hidden' | 'visible' | 'prerender' | undefined
function getVisibility() {
if (!document) return 'visible'
return document.visibilityState
}
function useDocumentVisibility(): VisibilityState {
const [documentVisibility, setDocumentVisibility] = useState<VisibilityState>(getVisibility)
useEventListener(
'visibilitychange',
() => {
console.log(getVisibility())
setDocumentVisibility(getVisibility())
},
{
target: () => document,
}
)
return documentVisibility
}
- 实现比较简单,监听一下
visibilitychange
即可
useTitle
interface Options {
restoreOnUnmount?: boolean
}
const DEFAULT_OPTIONS: Options = {
restoreOnUnmount: false,
}
function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
const titleRef = useRef(document.title)
useEffect(() => {
document.title = title
}, [title])
useUnmount(() => {
if (options.restoreOnUnmount) {
document.title = titleRef.current
}
})
}
- 实现比较简单
- 第一次进入先把原有的title保存下来
- 挂载/当
title
改变后,修改title - 页面卸载后,根据
restore
判断是否恢复之前的title
useFavicon
import { useEffect } from 'react'
const ImgTypeMap = {
SVG: 'image/svg+xml',
ICO: 'image/x-icon',
GIF: 'image/gif',
PNG: 'image/png',
}
type ImgTypeKeys = keyof typeof ImgTypeMap
export function useFavicon(href: string) {
useEffect(() => {
if (!href) return
const curUrl = href.split('.')
const suffix = curUrl[curUrl.length - 1].toUpperCase() as ImgTypeKeys
// 1. 创建
const link: HTMLLinkElement = document.querySelector("link[rel*='icon']") || document.createElement('link')
link.type = ImgTypeMap[suffix]
link.rel = 'shortcut icon'
link.href = href
// 2. 挂载
document.querySelector('head')?.append(link)
}, [href])
}
useHover
interface Options {
onEnter: () => void
onLeave: () => void
onChange: (isHovering: boolean) => void
}
function useHover(target, options: Record<string, any> = {}): boolean {
const { onEnter, onLeave, onChange } = options
const [state, { setTrue, setFalse }] = useBoolean()
useEventListener(
'mouseleave',
() => {
onLeave?.()
onChange?.(false)
setFalse()
},
{
target,
}
)
useEventListener(
'mouseenter',
() => {
onEnter?.()
onChange?.(true)
setTrue()
},
{
target,
}
)
return state
}
实现较为简单,本质还是使用的useEventListener
,回调对应的函数即可
useMutationObserver
前置知识 MutationObserver
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MutationObserver</title>
</head>
<style>
.box {
width: 120px;
height: 120px;
background: pink;
}
</style>
<body>
<div class="box"></div>
<button>+10</button>
<script>
const boxEl = document.querySelector('.box')
const btnEl = document.querySelector('button')
const cb = ([item], observer) => {
console.log(item.target.style.width)
}
const observer = new MutationObserver(cb)
observer.observe(boxEl, {
attributes: true,
})
btnEl.addEventListener('click', () => {
const width = boxEl.clientWidth
boxEl.style.width = `${boxEl.clientWidth + 10}px`
})
</script>
</body>
</html>
走读代码
- 创建observer
- 监听boxEl的改变
当我们点击按钮的时候,增加
box
的宽度,当它改变就触发cb
回调函数
function useMutationObserver(callback: MutationCallback, target, options: MutationObserverInit = {}) {
const callbackRef = useLatest(callback)
useEffectWithTarget(
() => {
const el = getTargetElement(target)
if (!el) return
// 闭包问题 closure problem
// const observer = new MutationObserver(callbackRef.current)
const observer = new MutationObserver((...args) => callbackRef.current(...args))
observer.observe(el, options)
return () => {
observer.disconnect()
}
},
[options],
target
)
}
其中MutationObserver
接受的回调函数可能被缓存下来了,而在ahooks
源码中,存在闭包问题,拿到的是第一次的值,已经提交PR
useInViewport
前置知识
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
.wrap {
width: 150px;
height: 500px;
border: 1px solid red;
overflow: scroll;
}
.container {
width: 150px;
height: 3000px;
padding-top: 600px;
}
.scroll-area {
width: 100px;
height: 300px;
background: pink;
}
.box {
width: 100px;
height: 100px;
background: skyblue;
margin: -50% 0 -50% 0;
}
</style>
</head>
<body>
<div class="wrap">
<div class="container">
<div class="scroll-area">111</div>
</div>
</div>
<div class="box"></div>
<script>
const options = {
rootMargin: '0px',
threshold: 0.1,
}
const observer = new IntersectionObserver((e, obs) => {
for (const entry of e) {
console.log(entry.isIntersecting)
console.log(entry.intersectionRatio)
}
}, options)
observer.observe(document.querySelector('.scroll-area'))
</script>
</body>
</html>
效果如下:可以监听dom
在滚动区域中的状态,比如:可见型、阙值等
实现
本质:就是拿到需要监听的dom
以及IntersectionObserver
需要的options
传递即可
function useInViewport(target, options: Options = {}) {
const { callback, ...option } = options
const [inViewport, setInViewport] = useState<boolean>()
const [ratio, setRatio] = useState<number>()
useEffectWithTarget(
() => {
const targets = Array.isArray(target) ? target : [target]
const els = targets.map((target) => getTargetElement(target)).filter((el) => !!el)
if (!els.length) return
console.log(els)
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
setInViewport(entry.isIntersecting)
setRatio(entry.intersectionRatio)
callback?.(entry)
}
},
{ ...option, root: getTargetElement(option?.root) }
)
els.forEach((el) => {
observer.observe(el)
})
return () => {
observer.disconnect()
}
},
[options],
target
)
return [inViewport, ratio]
}
转载自:https://juejin.cn/post/7308219624138440742