手写swr,理解swr原理

一. swr介绍

swrReact数据请求的Hook,提供数据缓存机制,减少重复请求。

javascript 复制代码
import useSWR from 'swr'
const fetcher = (...args) => fetch(...args).then(res => res.json())

function App() {
  const { data, isLoading } = useSWR('/api/user', fetcher)
  
  if (isLoading) return <h1>Loading...</h1>
  
  const { user } = data
  
  return <h1>{user.name}</h1>
}

二. 实现swr

2.1 初始化cache

cacheMap类型数据,用于记录请求返回的数据,核心数据有dataerrorisValidatingisLoading

EVENT_REVALIDATORS用于收集校验请求的方法。

FETCH用于记录当前的请求实例,主要作用是减少重复请求和解决请求竞态问题。

javascript 复制代码
export const SWRGlobalState = new WeakMap()

const initCache = provider => {
  if (!SWRGlobalState.has(provider)) {
    // key为调用useSWR方法传入的第一个入参对应的字符串标识, value是组件更新渲染方法
    const subscriptions = {}

    // 每次修改cache数据时,会校验cache数据是否变更,变更需要触发组件重新更新渲染
    const setter = (key, value, prev) => {
      provider.set(key, value)

      const subs = subscriptions[key]
      if (subs) {
        subs.forEach(fn => fn(value, prev))
      }
    }

    // 订阅组件更新渲染方法
    const subscribe = (key, callback) => {
      if (!subscriptions[key]) subscriptions[key] = []
      const subs = subscriptions[key]
      subs.push(callback)

      return () => subs.splice(subs.indexOf(callback), 1)
    }
    
    const EVENT_REVALIDATORS = {}
    const MUTATION = {}
    const FETCH = {}
    const PRELOAD = {}

    SWRGlobalState.set(provider, [EVENT_REVALIDATORS, MUTATION, FETCH, PRELOAD, setter, subscribe])
  }

  return [provider]
}

const [cache] = initCache(new Map())

2.2 初始化默认配置

useSWR支持许多的配置项,本文只讨论用到的配置,不做过多扩展。

javascript 复制代码
export const defaultConfig = {
  cache,
  dedupingInterval: 2000, // 2s内相同请求会合并处理
}

2.3 定义useSWR方法

首先需要将useSWR第一个入参转换成对应的字符串标识,通过useSyncExternalStore方法收集组件更新渲染方法。

接着在useEffect回调方法请求接口数据,将请求实例记录到FETCH对象中,避免重复请求,当请求接口返回数据后缓存到cache中。

javascript 复制代码
function useSWR(_key, fetcher, config) {
  const { cache } = config
  const [EVENT_REVALIDATORS, , FETCH, , setter, subscribe] = SWRGlobalState.get(cache)
  // _key是任意数据类型,会转成成对应的字符串类型标识
  const [key, fnArg] = serialize(_key)

  // 只需关注data、error、isValidating、isLoading这四个字段的变化
  const isEqual = (current, prev) => {
    const keys = ['data', 'error', 'isValidating', 'isLoading']
    for (const key of keys) {
      if (!Object.is(current[key], prev[key])) return false
    }
    return true
  }

  // getter
  const getCache = () => {
    if (!cache.has(key)) {
      cache.set(key, {
        data: undefined,
        error: undefined,
        isValidating: true,
        isLoading: true,
      })
    }
    return cache.get(key)
  }

  // setter
  const setCache = data => {
    const prev = getCache()
    setter(key, { ...prev, ...data }, prev)
  }

  // subscribe
  const subscribeCache = callback =>
    subscribe(key, (current, prev) => {
      if (!isEqual(current, prev)) callback()
    })

  const cached = useSyncExternalStore(subscribeCache, getCache)

  const revalidate = async () => {
    if (!key) return
    // 请求返回数据
    let newData
    // 请求标识
    let timestamp
    const cleanup = () => {
      if (FETCH[key] && FETCH[key][1] === timestamp) delete FETCH[key]
    }
    const shouldStartNewRequest = !FETCH[key]
    try {
      if (shouldStartNewRequest) {
        setCache({ isValidating: true, isLoading: !cached.data })
        FETCH[key] = [fetcher(fnArg), getTimestamp()]
      }
      ;[newData, timestamp] = FETCH[key]
      // 获取请求数据
      newData = await newData
      if (shouldStartNewRequest) setTimeout(cleanup, config.dedupingInterval)
      // 只要FETCH不存在对应请求实例或者请求标识变了,说明该请求属于过期请求,不需要处理其返回数据
      if (!FETCH[key] || FETCH[key][1] !== timestamp) {
        return
      }
      setCache({
        data: newData,
        error: undefined,
        isValidating: false,
        isLoading: false,
      })
    } catch (error) {
      cleanup()
      setCache({ error, isValidating: false, isLoading: false })
    }
  }

  useEffect(() => {
   if (!EVENT_REVALIDATORS[key]) EVENT_REVALIDATORS[key] = []
    const revalidators = EVENT_REVALIDATORS[key]
    revalidators.push(revalidate)

    revalidate()

    return () => {
      const index = revalidators.indexOf(revalidate)
      if (index >= 0) {
        // O(1): faster than splice
        revalidators[index] = revalidators[revalidators.length - 1]
        revalidators.pop()
      }
    }
  }, [key])

  return { ...cached, mutate: internalMutate.bind(cache, key) }
}

export default (key, fetcher, config) =>
  useSWR(key, fetcher, { ...defaultConfig, ...config })

2.4 定义mutate方法

mutate方法主要用于修改缓存数据以及触发校验请求方法,返回值为最新的cache数据

javascript 复制代码
const internalMutate = (cache, key, data) => {
  const [EVENT_REVALIDATORS, , FETCH] = SWRGlobalState.get(cache)
  const [getCache, setCache] = createCacheHelper(cache, key)

  const startRevalidate = () => {
    const revalidators = EVENT_REVALIDATORS[key]
    // 将请求实例缓存清空掉确保能够重新发起请求
    delete FETCH[key]
    revalidators[0]().then(() => getCache().data)
  }

  // 如果data为undefined,直接重新校验请求即可
  if (data === undefined) {
    return startRevalidate()
  }

  setCache({ data })
  startRevalidate()
  return data
}

三. 总结

swr通过cache缓存请求数据,通过FETCH缓存请求实例,减少重复请求,通过mutate方法手动更新缓存数据,触发校验请求逻辑。代码仓库

创作不易,如果文章对你有帮助,那点个小小的赞吧,你的支持是我持续更新的动力!

相关推荐
Stestack5 分钟前
Python 给 Excel 写入数据的四种方法
前端·python·excel
SRC_BLUE_178 分钟前
[Web 安全] PHP 反序列化漏洞 —— PHP 序列化 & 反序列化
前端·安全·web安全·php
石兴稳12 分钟前
SSD 固态硬盘存储密度的分区
开发语言·javascript·数据库
念九_ysl23 分钟前
前端排序算法完全指南:从理论到实践
开发语言·javascript·算法·ecmascript
IT猿手29 分钟前
智能优化算法:雪橇犬优化算法(Sled Dog Optimizer,SDO)求解23个经典函数测试集,MATLAB
开发语言·前端·人工智能·算法·机器学习·matlab
windyrain1 小时前
基于 Ant Design Pro 实现表格分页与筛选参数的持久化
前端·javascript·react.js
懒人村杂货铺2 小时前
父子组件事件冒泡和捕获的顺序
前端·javascript·react.js
小刘不知道叫啥2 小时前
React 源码揭秘 | 更新队列
前端·react.js·前端框架
录大大i2 小时前
HTML之JavaScript DOM操作元素(1)
前端·javascript·html
huangkaihao2 小时前
无限滚动优化指南:从原理到实践
前端·面试·设计