背景
特别喜欢红宝书的一句话"站在巨人的肩上",起因是觉得react中的hooks语法,用了那么长时间,有的时候还是似懂非懂,所以才有了这一篇文章,学习一下前人封装hook的方法。渐进式学习,从浅入深,一点一点吃透,也希望大家能够从 理解思想 -> 输出思想,这样社区才能越来越好,帮助自己也帮助大家。如果文章有错误,也希望各位指点一二。
本文将渐进式更新...
前置知识
前言
- 如果
react hooks
语法不熟练,请确保看过一遍官方文档,为了让代码简洁,react hooks
将不再引入,当它存在即可 - 文章会直接跳过一些边界情况,
isFunc
,isBroswer
等,如有遗漏这些工具函数判断,自行判断下,以及大部分Ts
类型,单元测试等。把更多时间专注于hooks
的封装,避免造成一些心智负担 - 一些辅助函数
useLatest
,useMemoizedFn
,useUpdateEffect
等,就不再引入,默认当它存在即可 - 保留了大部分
ahooks
源码,但是我更改了一部分,我觉得更改了一部分源码,才知道你学习的ahooks
源码,这是我有意为之 - 最佳学习路线,照着ahooks官网,一遍看案例,一边对照源码学习,
有疑惑的地方先思考,再动手,动手,一定要动手
状态就像快照
ts
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 返回当前最新的值
ts
function useLatest<T>(value: T) {
const ref = useRef(value)
ref.current = value
return ref
}
- 解决闭包问题
ts
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
不会
ts
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
- 忽略首次执行,只有依赖发生改变才会执行
ts
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
- 组件初始化时执行
ts
const useMount = (fn: () => void) => {
useEffect(() => {
fn?.()
}, [])
}
useUnmount
- 组件卸载时执行
ts
const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn)
useEffect(
() => () => {
fnRef.current()
},
[]
)
}
useUnmountedRef
- 当前组件是否已经卸载
ts
const useUnmountedRef = () => {
const unmountedRef = useRef(false)
useEffect(() => {
unmountedRef.current = false
return () => {
unmountedRef.current = true
}
}, [])
return unmountedRef
}
State
useSetState
- 案例说明
tsx
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
重新定义(引用不同,如果传递给子组件,导致子组件进行不必要的执行)
- 可用于合并对象,基本与
ts
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
ts
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
ts
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
ts
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
ts
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
ts
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
ts
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
ts
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
ts
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
ts
function useGetState(initialState) {
const [state, setState] = useState(initialState)
const latestRef = useLatest(state)
const getState = useCallback(() => latestRef.current, [])
return [state, setState, getState]
}
- 获取最新的state
useResetState
ts
function useResetState(initialState) {
const [state, setState] = useState(initialState)
const resetState = useCallback(() => {
setState(initialState)
}, [])
return [state, setState, resetState]
}
- 利用
useCallback
, 每次当hook
重新执行,而resetState
中的回调函数拿到的还是初始化的值,利用闭包