Inside TanStack Query - QueryCache queries
看了 Inside React Query 後,想進一步了解:
- 是用什麼資料結構保存
Query
queryKey
如何對應到Query
程式碼是參考 v5.45.0
QueryCache queries
從 QueryClient
開始看起,可以在 QueryClient.prototype.constructor
發現,建立 QueryClient
實例時,同時也會建立 QueryCache
實例(如果沒額外提供)
class QueryClient {
#queryCache: QueryCache
// ...
constructor() {
this.#queryCache = config.queryCache || new QueryCache()
// ...
}
// ...
}
接著看到 QueryCache.prototype.constructor
,可以得知,QueryCache.queries
是用來存放 Query
實例的 Map
,key 為 string 型別
class QueryCache extends Subscribable<QueryCacheListener> {
#queries: QueryStore
constructor(public config: QueryCacheConfig = {}) {
super()
this.#queries = new Map<string, Query>()
}
// ...
}
Query key to query hash
知道 queries
是一個 Map
之後,想了解要如何新增一筆 Query
,在 QueryCache.add()
得知:
- 每個
Query
實例帶有queryHash
屬性 queryHash
為QueryCache.queries
的 key
class QueryCache extends Subscribable<QueryCacheListener> {
// ...
add(query: Query<any, any, any, any>): void {
if (!this.#queries.has(query.queryHash)) {
this.#queries.set(query.queryHash, query)
this.notify({
type: 'added',
query,
})
}
}
// ...
}
繼續深入 queryHash
,這裡方向是先觀察 QueryCache.add()
如何被調用,可以在 QueryCache.build()
中得知:
- 先透過
hashQueryKeyByOptions
將queryKey
轉換成queryHash
- 如果該
queryHash
在queries
查詢不到對應的Query
,則建立一個Query
實例並新增至queries
class QueryCache extends Subscribable<QueryCacheListener> {
#queries: QueryStore
build<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
client: QueryClient,
options: WithRequired<
QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey'
>,
state?: QueryState<TData, TError>,
): Query<TQueryFnData, TError, TData, TQueryKey> {
const queryKey = options.queryKey
const queryHash =
options.queryHash ?? hashQueryKeyByOptions(queryKey, options)
let query = this.get<TQueryFnData, TError, TData, TQueryKey>(queryHash)
if (!query) {
query = new Query({
cache: this,
queryKey,
queryHash,
options: client.defaultQueryOptions(options),
state,
defaultOptions: client.getQueryDefaults(queryKey),
})
this.add(query)
}
return query
}
}
繼續看到 hashQueryKeyByOptions
,這個 function 用途很單純:
- 判斷要用 query-core 預設的
hashKey
,或 user-land 設定的queryKeyHashFn
- 將
queryKey
轉換成queryHash
並回傳
補充:以 useQuery
為例,hashQueryKeyByOptions
收到的 options
,是在初始 QueryClient
設置的 defaultOptions.queries
及 useQuery
設置的 queryOptions
,合併後的結果
function hashQueryKeyByOptions<TQueryKey extends QueryKey = QueryKey>(
queryKey: TQueryKey,
options?: Pick<QueryOptions<any, any, any, any>, 'queryKeyHashFn'>,
): string {
const hashFn = options?.queryKeyHashFn || hashKey
return hashFn(queryKey)
}
預設的 hashKey
是使用 JSON.stringify
搭配 replacer parameter,針對物件進行 key 的排序,目的是:確保不會因為物件內 key 的順序不同,得到不一致的 hash
/**
* Default query & mutation keys hash function.
* Hashes the value into a stable hash.
*/
function hashKey(queryKey: QueryKey | MutationKey): string {
return JSON.stringify(queryKey, (_, val) =>
isPlainObject(val)
? Object.keys(val)
.sort()
.reduce((result, key) => {
result[key] = val[key]
return result
}, {} as any)
: val,
)
}
hashKey(['todos', { page: 1, status: 'done' }])
// '["todos",{"page":1,"status":"done"}]'
hashKey(['todos', { status: 'done', page: 1 }])
// '["todos",{"page":1,"status":"done"}]'
(hashKey(['todos', { page: 1, status: 'done' }])
=== hashKey(['todos', { status: 'done', page: 1 }]))
// true
Matching queries
紀錄 TanStack Query 如何透過 queryKey
找到對應的 Query
,像是:
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries({ queryKey: ['todos'] })
從 QueryClient.invalidateQueries()
開始看起,可以發現:調用 QueryCache.findAll()
得到 Query[]
class QueryClient {
// ...
invalidateQueries(
filters: InvalidateQueryFilters = {},
options: InvalidateOptions = {},
): Promise<void> {
return notifyManager.batch(() => {
this.#queryCache.findAll(filters).forEach((query) => {
query.invalidate()
})
if (filters.refetchType === 'none') {
return Promise.resolve()
}
const refetchFilters: RefetchQueryFilters = {
...filters,
type: filters.refetchType ?? filters.type ?? 'active',
}
return this.refetchQueries(refetchFilters, options)
})
}
// ...
}
QueryCache.findAll()
的實作,大致流程:
- 呼叫
QueryCache.getAll()
,將QueryCache.queries
內所有的Query
以Query[]
表示,這裡以allQueries
代稱 allQueries.filter((query) => matchQuery(filters, query))
class QueryCache extends Subscribable<QueryCacheListener> {
#queries: QueryStore
constructor(public config: QueryCacheConfig = {}) {
super()
this.#queries = new Map<string, Query>()
}
// ...
getAll(): Array<Query> {
return [...this.#queries.values()]
}
findAll(filters: QueryFilters = {}): Array<Query> {
const queries = this.getAll()
return Object.keys(filters).length > 0
? queries.filter((query) => matchQuery(filters, query))
: queries
}
// ...
}
從 matchQuery
中得知,如果 filters.queryKey
存在:
filters.exact
設定為true
,會以queryHash
比對,filters.exact
未設定或為false
,則呼叫partialMatchKey(query.queryKey, filters.queryKey)
進行queryKey
部分比對
function matchQuery(
filters: QueryFilters,
query: Query<any, any, any, any>,
): boolean {
const {
type = 'all',
exact,
fetchStatus,
predicate,
queryKey,
stale,
} = filters
if (queryKey) {
if (exact) {
if (query.queryHash !== hashQueryKeyByOptions(queryKey, query.options)) {
return false
}
} else if (!partialMatchKey(query.queryKey, queryKey)) {
return false
}
}
if (type !== 'all') {
const isActive = query.isActive()
if (type === 'active' && !isActive) {
return false
}
if (type === 'inactive' && isActive) {
return false
}
}
if (typeof stale === 'boolean' && query.isStale() !== stale) {
return false
}
if (fetchStatus && fetchStatus !== query.state.fetchStatus) {
return false
}
if (predicate && !predicate(query)) {
return false
}
return true
}
以 partialMatchKey(['todos', { page: 1 }], ['todos'])
為例:
- 第一次直接比對兩個
queryKey
的陣列,會進到L10-L12
Object.keys(['todos'])
會得到['0']
export function partialMatchKey(a: QueryKey, b: QueryKey): boolean {
if (a === b) {
return true
}
if (typeof a !== typeof b) {
return false
}
if (a && b && typeof a === 'object' && typeof b === 'object') {
return !Object.keys(b).some((key) => !partialMatchKey(a[key], b[key]))
}
return false
}
補充:QueryCache.find
也使是透過 matchQuery
,不過預設將 filters.exact
設為 true
class QueryCache extends Subscribable<QueryCacheListener> {
// ...
find<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData>(
filters: WithRequired<QueryFilters, 'queryKey'>,
): Query<TQueryFnData, TError, TData> | undefined {
const defaultedFilters = { exact: true, ...filters }
return this.getAll().find((query) =>
matchQuery(defaultedFilters, query),
) as Query<TQueryFnData, TError, TData> | undefined
}
// ...
}
Summary
- 建立
QueryClient
實例時,同時也會建立QueryCache
(如果沒額外提供) QueryCache.queries
是用來存放Query
實例的Map
,key 為queryHash
queryHash
是由queryKey
轉換而來,預設是透過JSON.stringify
,如果queryKey
含有 serializable object,會對 key 進行排序,確保不會因為物件 key 順序不同,得到不一致的 hashQueryCache
在查詢Query
時,會先透過[...QueryCache.queries.values()]
轉換成Query[]
queryKey
精準查詢,是使用queryHash
比對queryKey
部分查詢,是使用queryKey
做部分比對