React SWR源碼解析筆記

React SWR 庫是由開發Next.js的同一團隊Vercel開源出來的一款工具。其功能主要是用來實現HTTP RFC 5861規範中名爲stale-while-revalidate的緩存失效策略。簡單來講,就是可以在獲取數據的時候能夠先從緩存中返回數據,而後再發送請求進行驗證,最後更新數據的效果。從而達到能夠提早更新UI的目的。在低網速、緩存可用的狀況下,能夠提高用戶體驗。 接下來的這篇文章,主要是對其源碼進行一些分析和學習。javascript

認識一下接口

swr 這個庫在使用過程當中,咱們主要是使用 useSWR 這個接口。java

輸入

useSWR 接口的輸入主要由如下參數組成:react

  • key: 用來標識緩存的key值,字符串或返回字符串的方法git

  • fetcher: 請求數據接口github

  • options: 配置參數,大頭, 具體參數以下typescript

suspense = false : enable React Suspense mode (details) fetcher = window.fetch : the default fetcher function initialData : initial data to be returned (note: This is per-hook) revalidateOnMount : enable or disable automatic revalidation when component is mounted (by default revalidation occurs on mount when initialData is not set, use this flag to force behavior) revalidateOnFocus = true : auto revalidate when window gets focused revalidateOnReconnect = true : automatically revalidate when the browser regains a network connection (via navigator.onLine ) refreshInterval = 0 : polling interval (disabled by default) refreshWhenHidden = false : polling when the window is invisible (if refreshInterval is enabled) refreshWhenOffline = false : polling when the browser is offline (determined by navigator.onLine ) shouldRetryOnError = true : retry when fetcher has an error (details) dedupingInterval = 2000 : dedupe requests with the same key in this time span focusThrottleInterval = 5000 : only revalidate once during a time span loadingTimeout = 3000 : timeout to trigger the onLoadingSlow event errorRetryInterval = 5000 : error retry interval (details) errorRetryCount : max error retry count (details) onLoadingSlow(key, config) : callback function when a request takes too long to load (see loadingTimeout ) onSuccess(data, key, config) : callback function when a request finishes successfully onError(err, key, config) : callback function when a request returns an error onErrorRetry(err, key, config, revalidate, revalidateOps) : handler for error retry compare(a, b) : comparison function used to detect when returned data has changed, to avoid spurious rerenders. By default, dequal/lite is used. isPaused() : function to detect whether pause revalidations, will ignore fetched data and errors when it returns true . Returns false by default.api

輸出

輸出主要有如下幾個數據:數組

  • data: 數據promise

  • error: 錯誤信息緩存

  • isValidating: 請求是否在進行中

  • mutate(data, shouldRevalidate): 更改緩存數據的接口

使用方式

先來看一下具體的使用方式:

import useSWR from 'swr'
function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}
複製代碼

其基本使用方式和日常的react hook是同樣的,經過傳遞一個做爲key的字符串和對應的fetcher接口來獲取對應的數據。

流程

瞭解了使用方式後,接下來來查看一下具體的代碼實現。 經過查看源碼,總體實現流程能夠分爲如下幾個步驟:

  1. 配置config: 此步驟主要用來處理用戶輸入,將其轉換成內部須要用到的處理參數。

  2. 先從cache獲取數據, 內存保存一個ref引用對象,用來指向上次的請求接口(輸入中的key跟請求引用進行綁定)。若是緩存更新或key更新,則須要從新獲取數據。

  3. 處理請求操做,並暴露對外接口。

function useSWR<Data = any, Error = any>( ...args: | readonly [Key] | readonly [Key, Fetcher<Data> | null] | readonly [Key, SWRConfiguration<Data, Error> | undefined] | readonly [ Key, Fetcher<Data> | null, SWRConfiguration<Data, Error> | undefined ] ): SWRResponse<Data, Error> {

// 處理參數,並序列化對應的key信息
const [_key, config, fn] = useArgs<Key, SWRConfiguration<Data, Error>, Data>(
  args
)
const [key, fnArgs, keyErr, keyValidating] = cache.serializeKey(_key)


// 保存引用
const initialMountedRef = useRef(false)
const unmountedRef = useRef(false)
const keyRef = useRef(key)


// 此處爲從緩存中獲取數據,若是緩存中沒有對應數據,則從配置的initialData中獲取
const resolveData = () => {
  const cachedData = cache.get(key)
  return cachedData === undefined ? config.initialData : cachedData
}
const data = resolveData()
const error = cache.get(keyErr)
const isValidating = resolveValidating()

// 中間省略,主要爲方法定義邏輯

// 在組件加載或key變化時觸發數據的更新邏輯,並添加一些事件監聽函數
useIsomorphicLayoutEffect(() => {
    //... 省略
}, [key, revalidate])

// 輪詢處理,主要用以處理參數中的一些輪詢配置
useIsomorphicLayoutEffect(() => {}, [
config.refreshInterval,
config.refreshWhenHidden,
config.refreshWhenOffline,
revalidate
])


// 錯誤處理
if (config.suspense && data === undefined) {
  if (error === undefined) {
    throw revalidate({ dedupe: true })
  }
  throw error
}

// 最後返回狀態信息, 此處邏輯見狀態管理部分
}
複製代碼

config的邏輯

對於用戶輸入部分,defaultConfig + useContext + 用戶自定義config 優先級關係爲: defaultConfig < useContext < 用戶自定義config

export default function useArgs<KeyType, ConfigType, Data>( args: | readonly [KeyType] | readonly [KeyType, Fetcher<Data> | null] | readonly [KeyType, ConfigType | undefined] | readonly [KeyType, Fetcher<Data> | null, ConfigType | undefined] ): [KeyType, (typeof defaultConfig) & ConfigType, Fetcher<Data> | null] {

// 此處用來處理config等參數
  const config = Object.assign(
    {},
    defaultConfig,
    useContext(SWRConfigContext),
    args.length > 2
      ? args[2]
      : args.length === 2 && typeof args[1] === 'object'
      ? args[1]
      : {}
  ) as (typeof defaultConfig) & ConfigType
複製代碼

從新更新數據的邏輯

revalidate 從新更新數據, 在組件加載後或者當前狀態處於空閒時, 會從新更新數據。 須要處理depupe: 消重邏輯,即在短期內相同的請求須要進行去重。 聲明瞭一個CONCURRENT_PROMISES變量用來保存全部須要並行的請求操做。

const revalidate = useCallback(
  async (revalidateOpts: RevalidatorOptions = {}): Promise<boolean> => {
    if (!key || !fn) return false
    if (unmountedRef.current) return false
    if (configRef.current.isPaused()) return false
    const { retryCount = 0, dedupe = false } = revalidateOpts

    let loading = true
    let shouldDeduping =
      typeof CONCURRENT_PROMISES[key] !== 'undefined' && dedupe


    try {
      cache.set(keyValidating, true)
      setState({
        isValidating: true
      })
      if (!shouldDeduping) {
        broadcastState(
          key,
          stateRef.current.data,
          stateRef.current.error,
          true
        )
      }

      let newData: Data
      let startAt: number

      if (shouldDeduping) {
        startAt = CONCURRENT_PROMISES_TS[key]
        newData = await CONCURRENT_PROMISES[key]
      } else {

        if (config.loadingTimeout && !cache.get(key)) {
          setTimeout(() => {
            if (loading)
              safeCallback(() => configRef.current.onLoadingSlow(key, config))
          }, config.loadingTimeout)
        }

        if (fnArgs !== null) {
          CONCURRENT_PROMISES[key] = fn(...fnArgs)
        } else {
          CONCURRENT_PROMISES[key] = fn(key)
        }

        CONCURRENT_PROMISES_TS[key] = startAt = now()

        newData = await CONCURRENT_PROMISES[key]

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

        safeCallback(() => configRef.current.onSuccess(newData, key, config))
      }

      if (CONCURRENT_PROMISES_TS[key] !== startAt) {
        return false
      }

      if (
        MUTATION_TS[key] !== undefined &&
        (startAt <= MUTATION_TS[key] ||
          startAt <= MUTATION_END_TS[key] ||
          MUTATION_END_TS[key] === 0)
      ) {
        setState({ isValidating: false })
        return false
      }

      // 設置緩存
      cache.set(keyErr, undefined)
      cache.set(keyValidating, false)

      const newState: State<Data, Error> = {
        isValidating: false
      }

      if (stateRef.current.error !== undefined) {
        newState.error = undefined
      }

      if (!config.compare(stateRef.current.data, newData)) {
        newState.data = newData
      }

      if (!config.compare(cache.get(key), newData)) {
        cache.set(key, newData)
      }

      // merge the new state
      setState(newState)

      if (!shouldDeduping) {
        // also update other hooks
        broadcastState(key, newData, newState.error, false)
      }
    } catch (err) {
      delete CONCURRENT_PROMISES[key]
      delete CONCURRENT_PROMISES_TS[key]
      if (configRef.current.isPaused()) {
        setState({
          isValidating: false
        })
        return false
      }
      // 從緩存中獲取錯誤信息
      cache.set(keyErr, err)

      if (stateRef.current.error !== err) {
        setState({
          isValidating: false,
          error: err
        })
        if (!shouldDeduping) {
          // 廣播狀態
          broadcastState(key, undefined, err, false)
        }
      }

      // events and retry
      safeCallback(() => configRef.current.onError(err, key, config))
      if (config.shouldRetryOnError) {
       // 重試機制,須要容許消重
        safeCallback(() =>
          configRef.current.onErrorRetry(err, key, config, revalidate, {
            retryCount: retryCount + 1,
            dedupe: true
          })
        )
      }
    }

    loading = false
    return true
  },
  [key]
)
複製代碼

另外,mutate接口是對外輸出的一個讓用戶顯式調用來觸發從新更新數據的接口。好比用戶從新登陸的時候,須要顯式從新更新全部數據,此時就能夠使用 mutate 接口。其實現邏輯以下:

async function mutate<Data = any>( _key: Key, _data?: Data | Promise<Data | undefined> | MutatorCallback<Data>, shouldRevalidate = true ): Promise<Data | undefined> {
  const [key, , keyErr] = cache.serializeKey(_key)
  if (!key) return undefined

  // if there is no new data to update, let's just revalidate the key
  if (typeof _data === 'undefined') return trigger(_key, shouldRevalidate)

  // update global timestamps
  MUTATION_TS[key] = now() - 1
  MUTATION_END_TS[key] = 0

  // 追蹤時間戳
  const beforeMutationTs = MUTATION_TS[key]

  let data: any, error: unknown
  let isAsyncMutation = false

  if (typeof _data === 'function') {
    // `_data` is a function, call it passing current cache value
    try {
      _data = (_data as MutatorCallback<Data>)(cache.get(key))
    } catch (err) {
      // if `_data` function throws an error synchronously, it shouldn't be cached
      _data = undefined
      error = err
    }
  }

  if (_data && typeof (_data as Promise<Data>).then === 'function') {
    // `_data` is a promise
    isAsyncMutation = true
    try {
      data = await _data
    } catch (err) {
      error = err
    }
  } else {
    data = _data
  }

  const shouldAbort = (): boolean | void => {
    // check if other mutations have occurred since we've started this mutation
    if (beforeMutationTs !== MUTATION_TS[key]) {
      if (error) throw error
      return true
    }
  }

  // if there's a race we don't update cache or broadcast change, just return the data
  if (shouldAbort()) return data

  if (data !== undefined) {
    // update cached data
    cache.set(key, data)
  }
  // always update or reset the error
  cache.set(keyErr, error)

  // 重置時間戳,以代表更新完成
  MUTATION_END_TS[key] = now() - 1

  if (!isAsyncMutation) {
    // we skip broadcasting if there's another mutation happened synchronously
    if (shouldAbort()) return 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, undefined, i > 0)
      )
    }
    // 返回更新後的數據
    return Promise.all(promises).then(() => {
      if (error) throw error
      return cache.get(key)
    })
  }
  // 錯誤處理
  if (error) throw error
  return data
}
複製代碼

緩存邏輯

對於緩存的增刪改查,swr源碼中專門對其作了個封裝,並採用訂閱—發佈模式監聽緩存的操做。 下面是cache文件: //cache.ts 文件

import { Cache as CacheType, Key, CacheListener } from './types'
import hash from './libs/hash'

export default class Cache implements CacheType {
  private cache: Map<string, any>
  private subs: CacheListener[]

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

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

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

  keys() {
    return Array.from(this.cache.keys())
  }

  has(key: Key) {
    const [_key] = this.serializeKey(key)
    return this.cache.has(_key)
  }

  clear() {
    this.cache.clear()
    this.notify()
  }

  delete(key: Key) {
    const [_key] = this.serializeKey(key)
    this.cache.delete(_key)
    this.notify()
  }

  // TODO: introduce namespace for the cache
  serializeKey(key: Key): [string, any, string, 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 {
      key = String(key || '')
    }

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

    return [key, args, errorKey, isValidatingKey]
  }

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

    let isSubscribed = true
    this.subs.push(listener)

    return () => {
      if (!isSubscribed) return
      isSubscribed = false
      const index = this.subs.indexOf(listener)
      if (index > -1) {
        this.subs[index] = this.subs[this.subs.length - 1]
        this.subs.length--
      }
    }
  }

  private notify() {
    for (let listener of this.subs) {
      listener()
    }
  }
}
複製代碼

從源碼上能夠看到,當緩存更新的時候,會觸發內部 notify 接口通知到全部訂閱了相關更新的處理函數,從而能夠更好地監聽到數據的變化。

狀態管理

SWR對外暴露的狀態是以響應式的方式進行處理,以便在後續數據更新的時候能觸發組件的自動更新。 具體代碼以下:

// 帶引用的狀態數據,在後續依賴更新時,將會自動觸發render
export default function useStateWithDeps<Data, Error, S = State<Data, Error>>( state: S, unmountedRef: MutableRefObject<boolean> ): [ MutableRefObject<S>, MutableRefObject<Record<StateKeys, boolean>>, (payload: S) => void ] {

  // 此處聲明一個空對象的狀態,獲取其setState,而後在後續須要從新渲染的時候,調用該方法。
  const rerender = useState<object>({})[1]

  const stateRef = useRef(state)
  useIsomorphicLayoutEffect(() => {
    stateRef.current = state
  })
  
  // 若是一個狀態屬性在組件的渲染函數中被訪問到,就須要在內部將其做爲依賴標記下來,以便在後續這些狀態數據更新的時候,可以觸發重渲染。
  const stateDependenciesRef = useRef<StateDeps>({
    data: false,
    error: false,
    isValidating: false
  })

  /* 使用setState顯式的方式去觸發狀態更新 */
  const setState = useCallback(
    (payload: S) => {
      let shouldRerender = false

      for (const _ of Object.keys(payload)) {
        // Type casting to work around the `for...in` loop
        // [https://github.com/Microsoft/TypeScript/issues/3500](https://github.com/Microsoft/TypeScript/issues/3500)
        const k = _ as keyof S & StateKeys

        // If the property hasn't changed, skip
        if (stateRef.current[k] === payload[k]) {
          continue
        }

        stateRef.current[k] = payload[k]

        // 若是屬性被外部組件訪問過,則會觸發從新渲染
        if (stateDependenciesRef.current[k]) {
          shouldRerender = true
        }
      }

      if (shouldRerender && !unmountedRef.current) {
        rerender({})
      }
    },
    // config.suspense isn't allowed to change during the lifecycle
    // eslint-disable-next-line react-hooks/exhaustive-deps
    []
  )

  return [stateRef, stateDependenciesRef, setState]
}



function useSWR<Data = any, Error = any>( ...args: | readonly [Key] | readonly [Key, Fetcher<Data> | null] | readonly [Key, SWRConfiguration<Data, Error> | undefined] | readonly [ Key, Fetcher<Data> | null, SWRConfiguration<Data, Error> | undefined ] ): SWRResponse<Data, Error> {
// 。。。。


const [stateRef, stateDependenciesRef, setState] = useStateWithDeps<
    Data,
    Error
  >(
    {
      data,
      error,
      isValidating
    },
    unmountedRef
  )

//...


// 最終返回的狀態,是作了響應式包裝的數據,當訪問狀態數據的時候,會更新依賴
const state = {
    revalidate,
    mutate: boundMutate
  } as SWRResponse<Data, Error>
  
  Object.defineProperties(state, {
    data: {
      get: function() {
        stateDependenciesRef.current.data = true
        return data
      },
      enumerable: true
    },
    error: {
      get: function() {
        stateDependenciesRef.current.error = true
        return error
      },
      enumerable: true
    },
    isValidating: {
      get: function() {
        stateDependenciesRef.current.isValidating = true
        return isValidating
      },
      enumerable: true
    }
  })

   return state

}
複製代碼
相關文章
相關標籤/搜索