手把手教你写React hook请求

SWR 做为一个基于 react hook 的请求库, 在刚推出时便一晚上爆火, 从目前 issue 的解决速度和功能来看, 是一个不错的库.前端

官方文档: github.com/zeit/swr

本篇文章结合 API 深刻浅出了解SWR源码的实现.react

PS: 本文阅读大概须要18分钟时间.如对细节不关心可直接查看“有意思的实现” 了解做者作的优化点.git

目录github

  • 解析参数
  • 依赖处理
  • 全局配置
  • 请求数据逻辑
  • 回调处理
  • 循环请求
  • 缓存处理
  • 总结

解析参数

从 API 来看, 接收3个参数, 咱们再看看源码算法

if (args.length >= 1) {
    _key = args[0]
}
if (typeof args[1] === 'function') {
    fn = args[1]   //若是第二个参数是函数, 则赋值给 fn 
} else if (typeof args[1] === 'object') {
    config = args[1] //若是第二个参数是对象, 则赋值给 config 
}
if (typeof args[2] === 'object') {
    config = args[2]
}
if (typeof fn === 'undefined') {
    fn = config.fetcher  //若没传 fn , 则使用默认配置的函数
}
复制代码

从这段代码能够看出, 调用能够传 fetcher, 也能够不传 fetcher , 能够将 fetcher 的参数做为数组传递过去, 让调用更加灵活. 也将请求和经常使用的处理逻辑分隔开来. 达到解耦的效果.api

const { data } = useSWR( key, fetcher, options)
const { data } = useSWR('/api/user', { refreshInterval: 0 }) 
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher) //条件取key值
const { data } = useSWR(() => '/api/projects?uid=' + id) //key 为函数
useSWR(['/api/user', params], fetcher) // key 为数组
复制代码

第一个参数 key 能够传递字符串, 能够传递数组, 也能够传递一个函数.数组

const getKeyArgs = key => {
  let args = null
  if (typeof key === 'function') {
    try {
      key = key()
    } catch (err) {
      // 若是拿到依赖的值仍未解析到, 会抛出错误,则表示依赖还未好
      key = ''
    }
  }
  if (Array.isArray(key)) {
    args = key
    key = hash(key)
  } else {
    key = String(key || '') // 若解析不到值, 则表示依赖未好或使用方式错误
  }

  return [key, args] // 解析到的参数做为第二个函数的入参
}
复制代码

tips: 直接用 key 值来作惟一的标识, 若是传递的数组中有对象,每次都会从新建立对象, 都是一个不同的值,key值发生变化,就会进入死循环. 解决方法是使用 useMemo, 以下:浏览器

const params = useMemo(() => ({ id }), [id])
useSWR(['/api/user', params], fetcer)
复制代码

依赖处理

这里有一个颇有意思的地方.一样的, 咱们先看看 API. 第二个请求必定能够保证在第一个请求结束后再发出.缓存

const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
复制代码
有意思的实现

用短短几行代码, 便实现了一个依赖请求. 在解析参数的时候, 若拿不到依赖的值, 则报错进入catch流程. 以下:bash

if (typeof key === 'function') {
    try {
      key = key()
    } catch (err) {
      // 若是拿到依赖的值仍未解析到, 会抛出错误,则表示依赖还未好
      key = ''
    }
}
复制代码

在处理获取接口的函数里. 若 key 为 ‘’ , 则不作任何处理, 当依赖的接口返回了数据, 第二个接口的依赖值 key 发生了改变, 便从新触发发起请求.

const revalidate = useCallback(async(revalidateOpts) => {
    if (!key) return false //没有key值则直接返回
},[key]) // 当依赖项 key 变化时 useCallback 会从新执行
复制代码

全局配置

接下来咱们看到配置参数的获取和初始化, 这里除了 swr 本身默认的 defaultConfig 以外, 还使用了 useContext .

const defaultConfig: ConfigInterface = {
  onLoadingSlow: () => {},
  onSuccess: () => {},
  onError: () => {},
  onErrorRetry,
  errorRetryInterval: 5 * 1000,
  focusThrottleInterval: 5 * 1000,
  dedupingInterval: 2 * 1000,
  loadingTimeout: 3 * 1000,
  refreshInterval: 0,
  revalidateOnFocus: true,
  refreshWhenHidden: false,
  shouldRetryOnError: true,
  suspense: false
}
config = Object.assign(
    {},
    defaultConfig,
    useContext(SWRConfigContext),
    config
)
复制代码

其实按照以往写公共组件的方法, 咱们可能会用类, 而后写一个静态 static 方法, 让用户调用这个静态方法, 全局初始化数据. 但 swr 明显不可能使用 class , 这里使用了 Context 来配置全局数据共享.
咱们依然结合他的 api 来看.

import useSWR, { SWRConfig } from 'swr'
function App () {
  return (
    <SWRConfig 
      value={{
        refreshInterval: 3000,
      }}
    >
      组件..
    </SWRConfig>
  )
}
复制代码

有意思的实现

从 swr 里面引入了 SWRConfig ,那咱们找一下源码里面的内容. 对外暴露了一个 Provider, 外部直接接收一个 value 属性, 内部使用 useContext(SWRConfigContext) 获取对应的参数.

const SWRConfigContext = createContext<ConfigInterface>({}) 
SWRConfigContext.displayName = 'SWRConfigContext'
const SWRConfig = SWRConfigContext.Provider
export { SWRConfig }
复制代码

小tip: 若本身尝试封装基于 hook 的组件, 通用的配置方式能够参考这种方式.

请求数据逻辑

先列一个大概的轮廓, 核心的步骤以下:

  1. 将相关的请求处理放在 revalidate 中,用 useCallback 根据 key 值作缓存. key 值为入参.
  2. 在 useIsomorphicLayoutEffect 时,如有缓存则优先使用缓存数据, 再异步调用并更新数据.
let [state, dispatch] = useReducer(mergeState, {
    data: initialData,
    error: initialError,
    isValidating: false
}) // 使用 reducer 的方式修改数据
const [key, fnArgs] = getKeyArgs(_key) //内部的代码在参数解析部分有讲
const unmountedRef = useRef(false)  //缓存 mounted 的状态
const revalidate = useCallback(async(revalidateOpts) => {
    if (unmountedRef.current) return false
    try {
        // 请求超时触发 onLoadingSlow 回调函数
        // 将请求记录到 CONCURRENT_PROMISES 对象
        if (fnArgs !== null) {
            CONCURRENT_PROMISES[key] = fn(...fnArgs) //将传的参数传递过去
        } else {
            CONCURRENT_PROMISES[key] = fn(key)
        }
        // 执行请求
        newData = await CONCURRENT_PROMISES[key]
        // 请求成功时的回调
        config.onSuccess(newData, key, config)
        // 将请求结果存储到缓存 cache 中
        cacheSet(key, newData)
        // 对比新旧数据,若数据发生改变, 则批量改变数据
        if (deepEqual(dataRef.current, newData)) {} else {
            newState.data = newData
            dataRef.current = newData
        }
        dispatch(newState)
    }catch(err) {
        // 请求失败设置值
        // 请求失败时回调
        // 根据配置判断是否重试请求
    }
},[key]) // 当依赖项 key 变化时 useCallback 会从新执行
useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect //根据服务端客户端选择 useEffect 仍是 useLayoutEffect
useIsomorphicLayoutEffect(() => {
    unmountedRef.current = false
    const softRevalidate = () => revalidate()
    if (
      typeof latestKeyedData !== 'undefined' &&
      window['requestIdleCallback']
    ) {
      // 若是有缓存则延迟从新验证,优先使用缓存数据进行渲染
      window['requestIdleCallback'](softRevalidate)
    } else {
      softRevalidate()
    }
    
    // 窗口聚焦时,从新验证
    // 注册全局缓存的更新监听函数
    // 根据配置的 refreshInterval 循环请求
    return () => {
        // 清除反作用
        unmountedRef.current = true
        ...
    }
}, [key, config.refreshInterval, revalidate])
return {
    error,
    data,
    revalidate,
    isValidating
}
复制代码

有意思的实现

  1. 以前版本的 swr 使用的是 unstable_batchedUpdates 来进行批量处理. . 此处使用了 useReducer 来优化批量修改数据, 若无使用此方法, 会致使修改 loading, data 数据引发了2次渲染.
  2. 使用 useCallback 接收key值, 若key值不变, 则可使用上次的函数. 提升了性能.
  3. 使用 useRef 来存储 unmountedRef 值, 在组件卸载的时候, 再也不 setState, 解决了组件卸载以后, 接口请求回来 setState 的错误.
  4. 使用 useLayoutEffect 在 ui 渲染前开始触发请求, 稍微提升了速度. 而且兼容了服务端渲染的状况.
  5. 请求函数被 requestIdleCallback 包裹, 非阻塞.
  6. 将发起请求的处理放在 revalidate 中, 方便回调及各类调用.

回调处理

在请求的时候, 咱们常常有一些定制化的需求, 好比“接口错误后须要展现什么数据”, “接口错误后重试多少次”, 若是每一个接口都须要写一遍, 那真的太难受了. 一样的, 咱们看一下调用的 API

useSWR(key, fetcher, {
  onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
    if (retryCount >= 10) return
    if (error.status === 404) return

    // 5秒后重试
    setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
  }
})
复制代码

这块的源码很简单. 就是在接口错误的时候, 调用传递进来的 onErrorRetry 函数.

if (config.shouldRetryOnError) {
    const retryCount = (revalidateOpts.retryCount || 0) + 1
    config.onErrorRetry(err,key,config, revalidate, Object.assign({ dedupe: true }, revalidateOpts, { retryCount })) // 默认设置 dedupe 为 true , 配置能够设置在指定时间内若是发起一样的请求, 则不请求. 
}
复制代码

这里注意的是, 若你不重写onErrorRetry函数,则按照它默认的方式(指数回退算法)来处理. emmm……源码以下:

const count = Math.min(opts.retryCount || 0, 8);
const timeout =
  ~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;
复制代码

循环请求

跟 error 回调错误处理同样, 这里都是默认 dedupe 为 true .

useIsomorphicLayoutEffect(() => {
const softRevalidate = () => revalidate({ dedupe: true })
let timeout = null
if (config.refreshInterval) {
    const tick = async () => {
        if (!errorRef.current && (config.refreshWhenHidden || isDocumentVisible())
        ) {
          // 页面可视时, 且接口未错误时, 发起请求. //若是你想让接口错误的时候也继续循环, 那得在 onError 里面处理
          await softRevalidate()
        }
        const interval = config.refreshInterval
        timeout = setTimeout(tick, interval)
    }
    timeout = setTimeout(tick, config.refreshInterval)
}
}, [key, config.refreshInterval, revalidate]) //循环参数发生变化时,从新执行
复制代码

从上面基本的轮廓已经能够看到是如何发起一个请求, 以及如何拿到数据. 倘若你的 hook 请求库不须要作缓存, 使用上面的轮廓基本上就能够知足业务需求了.

缓存处理

不过很明显,这个库主打的是 “stale-while-revalidate ”: 旨在经过缓存提升用户体验。其核心就是容许客户端先使用缓存中不新鲜的数据,而后在后台异步从新验证更新缓存,等下次使用的时候数据就是新的了, 即在请求以前先从缓存返回数据(stale),而后在异步发送请求,最后当数据返回时更新缓存并触发 UI 的从新渲染,从而提升用户体验。

有意思的实现

核心: 由于咱们须要使用到缓存(旧的数据),必然得有个变量来存储接口返回的数据于内存中.

const initialData =
    (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
const initialError = shouldReadCache ? cacheGet(keyErr) : undefined
const revalidate = useCallback(
    async () => {
    try {
        //发起请求
        const newData = await CONCURRENT_PROMISES[key] //存入并发的数组中, 可过参数dedupingInterval控制并发量
        // 将请求结果存储到缓存 cache 中
        cacheSet(key, newData)
        cacheSet(keyErr, undefined)
        keyRef.current = key
    }catch(err) {
        cacheSet(keyErr, err)
        keyRef.current = key
    }
    }, [key])
useIsomorphicLayoutEffect(() => {
const latestKeyedData = cacheGet(key) || config.initialData
// 若是有最新的数据, 则等浏览器空闲的时候再从新发起请求. 
if (
      typeof latestKeyedData !== 'undefined' &&
      !IS_SERVER &&
      window['requestIdleCallback']
    ) {
      window['requestIdleCallback'](softRevalidate)
    } else {
      softRevalidate()
    }
}, [key, config.refreshInterval, revalidate]) 
复制代码

最后

  • 欢迎加我微信(A18814127),拉你进技术群,长期交流学习...
  • 欢迎关注「前端加加」,认真学前端,作个有专业的技术人...

相关文章
相关标签/搜索