SWRのコードリーディング
コードを読んで気になった点をメモ
keyの比較
keyの比較は、stableHashという関数により、深い比較を行うようになっている。実装は下記。
深い比較を行うために、SWRは与えられたキーをhash化する。
たとえば、keyが{ a: 'b', c: [2, 3] }だった場合、#c:@2,3,,a:"b",のようにhashになる。
また、条件付きfetchのために関数がkeyとして与えられた場合、その関数を一旦実行してみる。
それがエラーになれば、まだfetchすべきでないと判断し、keyを空文字列に。
エラーにならなければ、関数の返り値をhash化する。
ちなみに、useSWRの返り値のdataの比較にも、デフォルトでこのstableHashが使われており、
dataオブジェクトの参照が変化しても、dataは変化したとみなされず、re-renderがトリガされないようになっている。
キャッシュの持ち方
SWRが内部で持っているキャッシュは、cache providerに保存される。
デフォルトでは、Mapがキャッシュプロバイダとして使われる。
キャッシュの中身
providerに保持されるキャッシュは、下記のようにdata, error, isValidating, isLoadingからなるState。
export type State<Data = any, Error = any> = {
data?: Data
error?: Error
isValidating?: boolean
isLoading?: boolean
}
SWRGlobalState
キャッシュプロバイダは、SWRGlobalStateというものに保存される。
SWRGlobalStateは、SWR全体で共有するWeakMap。
SWRGlobalStateは、キャッシュプロバイダをキーとして、下記のようなものを保持する。
[
Record<string, RevalidateCallback[]>, // EVENT_REVALIDATORS
Record<string, [number, number]>, // MUTATION: [ts, end_ts]
Record<string, [any, number]>, // FETCH: [data, ts]
Record<string, FetcherResponse<any>>, // PRELOAD
ScopedMutator, // Mutator
(key: string, value: any, prev: any) => void, // Setter
(key: string, callback: (current: any, prev: any) => void) => () => void // Subscriber
]
下記それぞれの詳細(読み取れる範囲)。
- EVENT_REVALIDATORS
- keyごとに保持する。
- フォーカスイベントなど、revalidateを引き起こしうるイベントの発生時に呼ばれるrevalidate関数。コンポーネントのマウント時及びkeyの変更時に、
SWRGlobalStateに登録される。 - https://github.com/vercel/swr/blob/main/core/use-swr.ts#L534
- https://github.com/vercel/swr/blob/main/core/use-swr.ts#L571
- MUTATION
- keyごとに保持する。
- mutationの開始と終了のタイムスタンプを保持する。mutationのrace conditionの対応に使われている
- https://github.com/vercel/swr/blob/main/core/use-swr.ts#L396
- FETCH
- keyごとに保持する。
- 現在進行中のfetchを記録しておくもの。fetchを開始した時に、fetcherが返すPromiseと、fetch開始時刻を記録する。
- https://github.com/vercel/swr/blob/main/core/use-swr.ts#L361
- これもrace conditionの対応に使われている。fetchが完了したとき、他の新しいfetchがすでに走っていたら、fetchの結果を破棄する
- https://github.com/vercel/swr/blob/main/core/use-swr.ts#L378
- PRELOAD
- 飛ばす
- ScopedMutator
- Setter
- providerの指定のkeyにキャッシュをセットする。また、後述のSubscriberで、そのkeyに対してcallbackが登録されていたらそれを発火させる
- https://github.com/vercel/swr/blob/main/_internal/utils/cache.ts#L64
- Subscriber
- 指定のkeyのキャッシュが変化したら発火するcallbackを登録する
- https://github.com/vercel/swr/blob/main/_internal/utils/cache.ts#L54
Dependency Collectionの仕組み
useSWRの返り値のうち、必要なものだけをコンポーネント側で取得することで、それ以外の返り値の変更時にre-renderingが走らないという仕組みがある。
これを実現する仕組みは下記。
-
useSyncExternalStoreを使って、下記設定を行う。- 設定
- cache providerが保持するstateが変化し、
- かつ
isEqual関数の結果がfalseなら、 - stateのスナップショット取得処理を呼び、
- それを
cached変数に格納する
- https://github.com/vercel/swr/blob/main/core/use-swr.ts#L203
- ここで使っている
subscribeCacheという関数は、前述のSWRGlobalStateのSubscriberのラッパーで、キャッシュに保存されたstateが変化したときに呼ばれるコールバックを登録するもの。 -
useSyncExternalStoreの仕様的には、スナップショット取得処理が呼ばれた時点でre-renderをトリガする感じになる。
- 設定
-
isEqual関数では、stateのうち、stateDependenciesに入っているプロパティのみをチェックしている。- https://github.com/vercel/swr/blob/main/core/use-swr.ts#L109
-
stateDependenciesは、単にuseRefで作っているオブジェクト。
-
useSWRの返り値のプロパティがなんらかの形でアクセスされると、get関数が走るようになっており、そこでstateDependenciesにプロパティが追加される形になっている。
つまり、例えば、 useSWRの返り値のisValidatingになんらかの形でアクセスすると、
- get関数の中で
stateDependenciesにisValidatingが追加される - その後、
isValidatingが変化すると、useSyncExternalStoreに登録しておいた処理の中で、isEqualがfalseになり、スナップショット取得処理が実行される - これにより、コンポーネントがre-renderされる
という流れになる。