前端人 精学ahooks源码

背景

特别喜欢红宝书的一句话"站在巨人的肩上",起因是觉得react中的hooks语法,用了那么长时间,有的时候还是似懂非懂,所以才有了这一篇文章,学习一下前人封装hook的方法。渐进式学习,从浅入深,一点一点吃透,也希望大家能够从 理解思想 -> 输出思想,这样社区才能越来越好,帮助自己也帮助大家。如果文章有错误,也希望各位指点一二。

本文将渐进式更新...

前置知识

前言

  • 如果react hooks语法不熟练,请确保看过一遍官方文档,为了让代码简洁,react hooks将不再引入,当它存在即可
  • 文章会直接跳过一些边界情况,isFuncisBroswer等,如有遗漏这些工具函数判断,自行判断下,以及大部分Ts类型,单元测试等。把更多时间专注于hooks的封装,避免造成一些心智负担
  • 一些辅助函数useLatestuseMemoizedFnuseUpdateEffect等,就不再引入,默认当它存在即可
  • 保留了大部分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中的回调函数拿到的还是初始化的值,利用闭包
相关推荐
GISer_Jing4 分钟前
从0开始分享一个React项目:React-ant-admin
前端·react.js·前端框架
川石教育8 分钟前
Vue前端开发子组件向父组件传参
前端·vue.js·前端开发·vue前端开发·vue组件传参
豆子熊.1 小时前
外包干了3年,技术退步明显...
软件测试·selenium·测试工具·面试·职场和发展
GISer_Jing1 小时前
Vue前端进阶面试题目(二)
前端·vue.js·面试
乐闻x1 小时前
Pinia 实战教程:构建高效的 Vue 3 状态管理系统
前端·javascript·vue.js
weixin_431449681 小时前
web组态软件
前端·物联网·低代码·编辑器·组态
橘子味小白菜1 小时前
el-table的树形结构后端返回的id没有唯一键怎么办
前端·vue.js
前端Hardy2 小时前
HTML&CSS:比赛记分卡
前端·javascript·css·3d·html
疯狂的沙粒2 小时前
Vue项目开发 element-UI 前端实现 1到10排列选择的按钮
前端·vue.js·ui
刺客-Andy2 小时前
React第六节 组件属性prop的propTypes类型使用介绍
前端·javascript·react.js·typescript