React hooks中swr的原理和源码解析

授权转载自:JonyYu 

https://github.com/forthealllight/blog/issues/61

swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目中如何实战使用swr,并且会解析一下swr的原理。从原理出发读一读swr的源码

  • 什么是swr

  • swr的的源码


一、什么是swr

useSWR 是 react hooks 中一个比较有意思的组件,既可以作为请求库,也可以作为状态管理的缓存用,SWR 的名字来源于“stale-while-revalidate”, 是在HTTP RFC 5861标准中提出的一种缓存更新策略 :

首先从缓存中取数据,然后去真实请求相应的数据,最后将缓存值和最新值做对比,如果缓存值与最新值相同,则不用更新,否则用最新值来更新缓存,同时更新UI展示效果。

useSWR 可以作为请求库来用:

//fetch
import useSWR from 'swr'
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

//axios
const fetcher = url => axios.get(url).then(res => res.data)
function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

//graphql
import { request } from 'graphql-request'
const fetcher = query => request('https://api.graph.cool/simple/v1/movies', query)
function App () {
  const { data, error } = useSWR(
    `{
      Movie(title: "Inception") {
        releaseDate
        actors {
          name
        }
      }
    }`,
    fetcher
  )
  // ...
}

此外,因为相同的 key 总是返回相同的实例,在 useSWR 中只保存了一个 cache 实例,因此 useSWR 也可以当作全局的状态管理机。比如可以全局保存用户名称 :

import useSWR from 'swr';
function useUser(id: string) {
  const { data, error } = useSWR(`/api/user`, () => {
    return {
      name: 'yuxiaoliang',
      id,
    };
  });
  return {
    user: data,
    isLoading: !error && !data,
    isError: error,
  };
}
export default useUser;

具体的 swr 的用法不是本文的重点,具体可以看文档,本文用一个例子来引出对于 swr 原理的理解:

const sleep = async (times: number) => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve();
      }, times);
    });
};
const { data: data500 } = useSWR('/api/user', async () => {
    await sleep(500);
    return { a: '500 is ok' };
});
const { data: data100 } = useSWR('/api/user', async () => {
    await sleep(100);
    return { a: '100 is ok' };
});

上述的代码中输出的是 data100 和 data500 分别是什么?

答案是:

data100和data500都输出了{a:'500 is ok '}

原因也很简单,在swr默认的时间内(默认是 2000 毫秒),对于同一个 useSWRkey ,这里的 key‘/api/user’ 会进行重复值清除, 只始终 2000 毫秒内第一个 keyfetcher 函数来进行缓存更新。

带着这个例子,我们来深入读读 swr 的源码

二、swr的源码

我们从 useSWR 的 API 入手,来读一读 swr 的源码。首先在 swr 中本质是一种内存中的缓存更新策略,所以在 cache.ts 文件中,保存了缓存的 map

(1)cache.ts 缓存

class Cache implements CacheInterface {
 

  constructor(initialData: any = {}) {
    this.__cache = new Map(Object.entries(initialData))
    this.__listeners = []
  }

  get(key: keyInterface): any {
    const [_key] = this.serializeKey(key)
    return this.__cache.get(_key)
  }

  set(key: keyInterface, value: any): any {
    const [_key] = this.serializeKey(key)
    this.__cache.set(_key, value)
    this.notify()
  }

  keys() {
    
  }

  has(key: keyInterface) {
  
  }

  clear() {
  
  }

  delete(key: keyInterface) {
  
  }
  serializeKey(key: keyInterface): [string, any, string] {
    let args = null
    if (typeof key === 'function') {
      try {
        key = key()
      } catch (err) {
        // dependencies not ready
        key = ''
      }
    }

    if (Array.isArray(key)) {
      // args array
      args = key
      key = hash(key)
    } else {
      // convert null to ''
      key = String(key || '')
    }

    const errorKey = key ? 'err@' + key : ''

    return [key, args, errorKey]
  }

  subscribe(listener: cacheListener) {
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    let isSubscribed = true
    this.__listeners.push(listener)
    return () => {
       //unsubscribe
    }
  }

  // Notify Cache subscribers about a change in the cache
  private notify() {
   
  }

上述是 cache 类的定义,本质其实很简单,维护了一个 map 对象,以 key 为索引,其中key 可以是字符串,函数或者数组,将 key 序列化的方法为:serializeKey

 serializeKey(key: keyInterface): [string, any, string] {
    let args = null
    if (typeof key === 'function') {
      try {
        key = key()
      } catch (err) {
        // dependencies not ready
        key = ''
      }
    }

    if (Array.isArray(key)) {
      // args array
      args = key
      key = hash(key)
    } else {
      // convert null to ''
      key = String(key || '')
    }

    const errorKey = key ? 'err@' + key : ''

    return [key, args, errorKey]
  }

从上述方法的定义中我们可以看出:

  • 如果传入的 key 是字符串,那么这个字符串就是序列化后的 key

  • 如果传入的 key 是函数,那么执行这个函数,返回的结果就是序列化后的 key

  • 如果传入的 key 是数组,那么通过 hash 方法(类似 hash 算法,数组的值序列化后唯一)序列化后的值就是 key

此外,在 cache 类中,将这个保存了 keyvalue 信息的缓存对象 map ,保存在实例对象 this.__cache 中,这个 this.__cache 对象就是一个 map ,有set get等方法。

(2)事件处理

在swr中,可以配置各种事件,当事件被触发时,会触发相应的重新请求或者说更新函数。swr对于这些事件,比如断网重连,切换 tab 重新聚焦某个 tab 等等,默认是会自动去更新缓存的。

在swr中对事件处理的代码为

const revalidate = revalidators => {
    if (!isDocumentVisible() || !isOnline()) return

    for (const key in revalidators) {
      if (revalidators[key][0]) revalidators[key][0]()
    }
  }

  // focus revalidate
  window.addEventListener(
    'visibilitychange',
    () => revalidate(FOCUS_REVALIDATORS),
    false
  )
  window.addEventListener('focus', () => revalidate(FOCUS_REVALIDATORS), false)
  // reconnect revalidate
  window.addEventListener(
    'online',
    () => revalidate(RECONNECT_REVALIDATORS),
    false
)

上述 FOCUS_REVALIDATORSRECONNECT_REVALIDATORS 事件中保存了相应的更新缓存函数,当页面触发事件visibilitychange(显示隐藏)、focus(页面聚焦)以及online(断网重连)的时候会触发事件,自动更新缓存

(3)useSWR 缓存更新的主体函数

useSWR 是swr的主体函数,决定了如何缓存以及如何更新,我们先来看 useSWR 的入参和形参。

入参:

  • key : 一个唯一值,可以是字符串、函数或者数组,用来在缓存中唯一标识 key

  • fetcher : (可选) 返回数据的函数

  • options : (可选)对于 useSWR 的一些配置项,比如事件是否自动触发缓存更新等等。

出参:

  • data : 与入参 key 相对应的,缓存中相应 keyvalue

  • error : 在请求过程中产生的错误等

  • isValidating : 是否正在请求或者正在更新缓存中,可以做为 isLoading 等标识用。

  • mutate(data?, shouldRevalidate?) : 更新函数,手动去更新相应 keyvalue

从入参到出参,我们本质在做的事情,就是去控制 cache 实例,这个 map 的更新的关键是:

什么时候需要直接从缓存中取值,什么时候需要重新请求,更新缓存中的值

const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
const CONCURRENT_PROMISES = {}  //以key为键,value为新的通过fetch等函数返回的值
const CONCURRENT_PROMISES_TS = {} //以key为键,value为开始通过执行函数获取新值的时间戳

下面我们来看,缓存更新的核心函数:revalidate

  // start a revalidation
  const revalidate = useCallback(
    async (
      revalidateOpts= {}
    ) => {
      if (!key || !fn) return false
      revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts)
      let loading = true
      let shouldDeduping =
        typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe

      // start fetching
      try {
        dispatch({
          isValidating: true
        })

        let newData
        let startAt

        if (shouldDeduping) {
        
          startAt = CONCURRENT_PROMISES_TS[key]
          newData = await CONCURRENT_PROMISES[key]
          
        } else {
         
          if (fnArgs !== null) {
            CONCURRENT_PROMISES[key] = fn(...fnArgs)
          } else {
            CONCURRENT_PROMISES[key] = fn(key)
          }

          CONCURRENT_PROMISES_TS[key] = startAt = Date.now()

          newData = await CONCURRENT_PROMISES[key]

          setTimeout(() => {
            delete CONCURRENT_PROMISES[key]
            delete CONCURRENT_PROMISES_TS[key]
          }, config.dedupingInterval)

        }

        const shouldIgnoreRequest =
        
          CONCURRENT_PROMISES_TS[key] > startAt ||
          
          (MUTATION_TS[key] &&
         
            (startAt <= MUTATION_TS[key] ||
            
              startAt <= MUTATION_END_TS[key] ||
           
              MUTATION_END_TS[key] === 0))

        if (shouldIgnoreRequest) {
          dispatch({ isValidating: false })
          return false
        }

        cache.set(key, newData)
        cache.set(keyErr, undefined)

        // new state for the reducer
        const newState: actionType<Data, Error> = {
          isValidating: false
        }

        if (typeof stateRef.current.error !== 'undefined') {
          // we don't have an error
          newState.error = undefined
        }
        if (!config.compare(stateRef.current.data, newData)) {
          // deep compare to avoid extra re-render
          // data changed
          newState.data = newData
        }

        // merge the new state
        dispatch(newState)

        if (!shouldDeduping) {
          // also update other hooks
          broadcastState(key, newData, undefined)
        }
      } catch (err) {
        // catch err
      }

      loading = false
      return true
    },
    [key]
  )

上述代码已经通过简化, dispatch 就是更新 useSWR 返回值的函数:

const stateDependencies = useRef({
    data: false,
    error: false,
    isValidating: false
})
const stateRef = useRef({
    data: initialData,
    error: initialError,
    isValidating: false
})
let dispatch = useCallback(payload => {
let shouldUpdateState = false
for (let k in payload) {
  stateRef.current[k] = payload[k]
  if (stateDependencies.current[k]) {
    shouldUpdateState = true
  }
}
if (shouldUpdateState || config.suspense) {
  if (unmountedRef.current) return
  rerender({})
 }
}, [])

在上述的 dispath 函数中,我们根据需要去更新 stateRefstateRef 的返回值,就是最终 useSWR 的返回值,这里的 rerender 是一个react hooks中的强制更新的一个hook:

const rerender = useState(null)[1]

每次执行 rerender({}) 的时候,就会触发所在 hook 函数内组件的整体更新。其次我们还要再一次明确:

const CONCURRENT_PROMISES = {}  //以key为键,value为新的通过fetch等函数返回的值
const CONCURRENT_PROMISES_TS = {} //以key为键,value为开始通过执行函数获取新值的时间戳

接着来看 revalidate 更新函数的核心部分:

    let shouldDeduping =
        typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
    let newData
    let startAt

    if (shouldDeduping) {
    
      startAt = CONCURRENT_PROMISES_TS[key]
      newData = await CONCURRENT_PROMISES[key]
      
    } else {
     
      if (fnArgs !== null) {
        CONCURRENT_PROMISES[key] = fn(...fnArgs)
      } else {
        CONCURRENT_PROMISES[key] = fn(key)
      }

      CONCURRENT_PROMISES_TS[key] = startAt = Date.now()

      newData = await CONCURRENT_PROMISES[key]

      setTimeout(() => {
        delete CONCURRENT_PROMISES[key]
        delete CONCURRENT_PROMISES_TS[key]
      }, config.dedupingInterval)

    }

上述代码中, shouldDeduping 是用来判断是否需要去重的依据,从上述代码可以看出 config.dedupingInterval 的默认值是 2000 毫秒,也就是在 2000 毫秒内,对于同一个 key 会去重,也就是说,如果 2000 毫秒内,对于同一个 key ,同时发起了多个更新函数,那么会以第一次更新的结果为准。以 key 为键,记录每个 key 发起的时候的时间戳的数组是 CONCURRENT_PROMISES_TS ,而 CONCURRENT_PROMISES ,由此可以看出,更准确 的说法是:

一定时间内,去重后的key和value的值的集合,key是useSWR中的唯一key,也就是cache实例map的key,value就是最新的缓存中更新过的值。

(4)useSWR 中如何更新

根据上述的代码我们知道了更新函数是怎么样的,在内存中保存了 CONCURRENT_PROMISES_TS 这个对象,其 keycache 中的 keyvalue 为最新的值,那么如何在 CONCURRENT_PROMISES_TS 对象 key 所对应的值发生变化的时候,去更新 useSWR 实例的返回值,从而达到我们最终的缓存更新效果呢。

我们接着来看代码:

//保存对象
const CACHE_REVALIDATORS = {}

//具体更新函数
const onUpdate: updaterInterface<Data, Error> = (
  shouldRevalidate = true,
  updatedData,
  updatedError,
  dedupe = true
) => {
  // update hook state
  const newState: actionType<Data, Error> = {}
  let needUpdate = false

  if (
    typeof updatedData !== 'undefined' &&
    !config.compare(stateRef.current.data, updatedData)
  ) {
    newState.data = updatedData
    needUpdate = true
  }
  
  if (stateRef.current.error !== updatedError) {
    newState.error = updatedError
    needUpdate = true
  }
  //更新当前的stateRef
  if (needUpdate) {
    dispatch(newState)
  }

  if (shouldRevalidate) {
    return revalidate()
  }
  return false
}

//增加监听key
const addRevalidator = (revalidators, callback) => {
    if (!callback) return
    if (!revalidators[key]) {
      revalidators[key] = [callback]
    } else {
      revalidators[key].push(callback)
    }
}
addRevalidator(CACHE_REVALIDATORS, onUpdate)

//更新缓存的方法
const broadcastState: broadcastStateInterface = (key, data, error) =>    {
      const updaters = CACHE_REVALIDATORS[key]
      if (key && updaters) {
        for (let i = 0; i < updaters.length; ++i) {
          updaters[i](false, data, error)
        }
      }
  }

broadcastState 方法会在每一次更新 cachekey 的时候触发,而 CACHE_REVALIDATORS 保存了所有与 key 相关的更新函数,这里需要注意的是:

为什么CACHE_REVALIDATORS[key]的值是一个数组?

因为 useSWRkey ,同一个 key 可以有多个更新函数,因此 CACHE_REVALIDATORS[key] 是一个数组。

举例来说,在同一个组件中使用两个同名 key ,但是他们的更新函数不同,是被允许的:

 const { data: data500 } = useSWR('/api/user', async () => {
    await sleep(500);
    return { message: '500 is ok' };
 });
 const { data: data100 } = useSWR('/api/user', async () => {
    await sleep(100);
    return { message: '100 is ok' };
 });

(5)mutate 主动触发更新函数

了解了useSWR 中的更新,那么剩下的这个 mutate 就及其简单:

const mutate: mutateInterface = async ()=>{
  let data, error

  if (_data && typeof _data === 'function') {
    // `_data` is a function, call it passing current cache value
    try {
      data = await _data(cache.get(key))
    } catch (err) {
      error = err
    }
  } else if (_data && typeof _data.then === 'function') {
    // `_data` is a promise
    try {
      data = await _data
    } catch (err) {
      error = err
    }
  } else {
    data = _data
  }
  
  ....
  
  const updaters = CACHE_REVALIDATORS[key]
  if (updaters) {
    const promises = []
    for (let i = 0; i < updaters.length; ++i) {
      promises.push(updaters[i](!!shouldRevalidate, data, error, i > 0))
    }
    // return new updated value
    return Promise.all(promises).then(() => {
      if (error) throw error
      return cache.get(key)
    })
  }
}

简单的说就是拿到值,然后调用 const updaters = CACHE_REVALIDATORS[key] 数组中的每一个更新函数,更新相应的 useSWR 的值即可。这里 data 的值可以是直接从缓存中取,或者是手动传入(类似于乐观更新的方式)。

最后

欢迎关注「前端瓶子君」,回复「交流」加入前端交流群!

欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!

在这里(算法群),你可以每天学习一道大厂算法编程题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!

另外,每周还有手写源码题,瓶子君也会解答哟!

》》面试官也在看的算法资料《《

“在看和转发”就是最大的支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值