Vercel・SWRのコードを読んでみようの会

8 min read読了の目安(約7200字

はじめに

SWRのコードを読んだときのメモがてら、大まかな処理の解説をしていきます。

https://github.com/vercel/swr

どの処理を追うか?

基本となる、データ取得とデータ更新の処理を追います。
手元でクローンしたほうが追いやすいかもです。

注意

2021/5/23の状態のmasterの最新コードを参照しています。当たり前ですが、この解説は時とともに信憑性をなくしていきます。

データの取得

実際にuseSWRの data プロパティがどのようなデータ源から取得されリターンされているか追います。

const { data } = useSWR('api/todos', fetcher); // dataはどこからくるのか?

まずは全ての入口となるuseSWR関数を見てみましょう。

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 [_key, fn, config] = useArgs<Key, SWRConfiguration<Data, Error>, Data>(
    args
  )
  ...

こんな感じではじまり500行ほどuseSWRの関数の処理が続きます。

useSWRの末尾を見てみましょう。

  ... 
  
  const state = {
    revalidate,
    mutate: boundMutate
  } as SWRResponse<Data, Error>
  const currentStateDependencies = stateDependenciesRef.current
  Object.defineProperties(state, {
    data: {
      get: function() {
        currentStateDependencies.data = true
        return data
      },
      enumerable: true
    },
    error: {
      get: function() {
        currentStateDependencies.error = true
        return error
      },
      enumerable: true
    },
    isValidating: {
      get: function() {
        currentStateDependencies.isValidating = true
        return isValidating
      },
      enumerable: true
    }
  })

  // Display debug info in React DevTools.
  useDebugValue(data)

  return state
}

最後に state というオブジェクトに色々詰めてリターンしているのがわかります。
ここの state.data は実際にuseSWRからリターンされる値のことですね。
ではこの state.data を中心に追っていきます。

    data: {
      get: function() {
        currentStateDependencies.data = true
        return data
      },
      enumerable: true
    },

state.data というものは data というものを参照していますね。

この data は何かというと 246行目 resolveDate で取得しています。

  // Get the current state that SWR should return.
  const resolveData = () => {
    const cachedData = cache.get(key)
    return isUndefined(cachedData) ? config.initialData : cachedData
  }
  const data = resolveData()

cache.get(key) 来ましたね。SWRのキーと値はイメージ通りでJavaScriptのMapを使って実装されています。

ではこの cache の出処を見ていきます。

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 [_key, fn, config] = useArgs<Key, SWRConfiguration<Data, Error>, Data>(
    args
  )
  const cache = config.cache // ココ!
  
  ...

はい、初めのところに戻ってきました。 220行目です。

cache のもとになる configuseArgs から取得していますね。ではこのフックを見ましょう。

resolve-args.tsに移動します。

// Resolve arguments for SWR hooks.
// This function itself is a hook because it uses `useContext` inside.
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, Fetcher<Data> | null, (typeof defaultConfig) & ConfigType] {
  const config = {
    ...defaultConfig,
    ...useContext(SWRConfigContext),
    ...(args.length > 2
      ? args[2]
      : args.length === 2 && typeof args[1] === 'object'
      ? args[1]
      : {})
  } as (typeof defaultConfig) & ConfigType

  // In TypeScript `args.length > 2` is not same as `args.lenth === 3`.
  // We do a safe type assertion here.
  const fn = (args.length > 2
    ? args[1]
    : args.length === 2 && typeof args[1] === 'function'
    ? args[1]
    : // Pass fn as null will disable revalidate
    // https://paco.sh/blog/shared-hook-state-with-swr
    args[1] === null
    ? args[1]
    : config.fetcher) as Fetcher<Data> | null

  return [args[0], fn, config]
}

config が定義されていますね。まずは defaultConfig に飛びます。
config.tsを見てみましょう。

const defaultConfig = {
  ...
  
  cache: wrapCache(new Map()),

  ...
} as const

export default defaultConfig

ありました。空のMapがnewされていますね。

その後に上書きが走る useContext(SWRConfigContext) ですね。
そうです。SWRではコンテキストを使った値管理をしています。
当然 SWRConfigContextcache プロパティを持つのでグローバルに cache プロパティにアクセスが可能になります。

データの読み込みのまとめ

  • 値は全て単一のMapで管理されている
  • そのMapはContextを使って管理されている
  • Mapにキーアクセスしてdataプロパティがを返す

データの書き込み

cache.get でデータを読み込めるなら cache.set を見ればデータの書き込みが追えますね。

set 関数はいくつかの場所で呼ばれていますが、一番代表的な処理は use-swr.ts:301行目です。

処理が長いのでかいつまんで見ていきます。

...
  const revalidate = useCallback(async (revalidateOpts: RevalidatorOptions = {}): Promise<boolean> => {
    ...
      
    // Try構文を使用している。
    try { 
    
      ...
     
      if (fnArgs !== null) { // 346行目
        CONCURRENT_PROMISES[key] = fn(...fnArgs) // fn関数はfetcher関数のこと。これを評価することで非同期の値が取れる。
      } else {
        CONCURRENT_PROMISES[key] = fn(key)
      }
      
      CONCURRENT_PROMISES_TS[key] = startAt = now()
     
      // 非同期のfethcerをawaitすることで値を取得する
      newData = await CONCURRENT_PROMISES[key] // 354行目
      
      ...
      
      // 取得したデータをcacheにセット
      cache.set(key, newData) // 423行目
	
      // 値を他のフックにブロードキャストする
      // also update other hooks
      broadcastState(cache, key, newData, newState.error, false) // 431行目
      
      ...
    }
    ...
  }

細かい説明は省きますが、fetcher関数を評価してそれをcacheにセットする。そのあとブロードキャストをする流れが見えます。

また、この revalidate 関数はマウント時に呼ばれていて use-swr.ts:495行目でそれを確認できます。

  ...
  
  // After mounted or key changed.
  useIsomorphicLayoutEffect(() => {
    if (!key) return UNDEFINED

    // Not the inital render.
    const keyChanged = initialMountedRef.current
    const softRevalidate = () => revalidate({ dedupe: true }) //ここで呼ばれる
  ... 

この useIsomorphicLayoutEffect は下記のような実装になっています。

export const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect

シンプルな useEffect(サーバー時) or useLayoutEffect(クライアント時) ですね。

データの書き込みのまとめ

  • revalidate関数で主に書き込みがされている。
  • fetcher関数をawaitで評価し、cacheにセットしている。
  • その後 broadcastState関数で値をブロードキャストしている。
  • revalidate関数はマウント時に呼ばれている

おわりに

OSSのコードリーディングは楽しいですね。かなり細かい説明は省いていますが、大まかな流れを理解できたら幸いです。

僕が働いているログラスではフロントエンドエンジニア絶賛募集中ですのでもし興味あればぜひ!

https://job.loglass.jp/frontend