Inside TanStack Query - garbage collection
•6 min read
紀錄 TanStack Query 如何實作 garbage collection 機制,確保未使用的快取不會無限增長。
程式碼參考 v5.45.0
Query
從測試開始找線索,搜尋與 gcTime
相關測試,在 query.test.tsx
中發現:
query.test.tsx
test('should use the longest garbage collection time it has seen', async () => {
// ...
})
test('should be garbage collected when unsubscribed to', async () => {
// ...
})
test('should be garbage collected later when unsubscribed and query is fetching', async () => {
// ...
})
test('should not be garbage collected unless there are no subscribers', async () => {
// ...
})
test('queries should be garbage collected even if they never fetched', async () => {
// ...
})
知道與 Query
相關,所以先看到 Query
實作。Query
為 Removable
的子類別 (L6, L9)
query.ts
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Removable {
// ...
constructor(config: QueryConfig<TQueryFnData, TError, TData, TQueryKey>) {
super()
// ...
}
}
Removable
為一個抽象類別,可以看到 garbage collection 相關實作:
- 透過
setTimeout
設置 garbage collected 的定時器 (L15-L17) - 每個子類別都需實作
optionalRemove
方法 (L36)
updateGcTime
(L23-L26) 對應官方文件對 gcTime
的說明:
- Defaults to 5 * 60 * 1000 (5 minutes) or Infinity during SSR
- When different garbage collection times are specified, the longest one will be used.
removable.ts
import { isServer, isValidTimeout } from './utils'
export abstract class Removable {
gcTime!: number
#gcTimeout?: ReturnType<typeof setTimeout>
destroy(): void {
this.clearGcTimeout()
}
protected scheduleGc(): void {
this.clearGcTimeout()
if (isValidTimeout(this.gcTime)) {
this.#gcTimeout = setTimeout(() => {
this.optionalRemove()
}, this.gcTime)
}
}
protected updateGcTime(newGcTime: number | undefined): void {
// Default to 5 minutes (Infinity for server-side) if no gcTime is set
this.gcTime = Math.max(
this.gcTime || 0,
newGcTime ?? (isServer ? Infinity : 5 * 60 * 1000),
)
}
protected clearGcTimeout() {
if (this.#gcTimeout) {
clearTimeout(this.#gcTimeout)
this.#gcTimeout = undefined
}
}
protected abstract optionalRemove(): void
}
從 Query.optionalRemove()
得知:
- 如果
Query
沒有任何QueryObserver
訂閱,且state.fetchStatus === 'idle'
即視為未使用 - 如果
Query
滿足上述條件,則從QueryCache
中刪除
query.ts
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Removable {
// ...
cache: QueryCache
state: QueryState<TData, TError>
observers: Array<QueryObserver<any, any, any, any, any>>
// ...
protected optionalRemove() {
if (!this.observers.length && this.state.fetchStatus === 'idle') {
this.#cache.remove(this)
}
}
// ...
}
接著看到 Query
設置 garbage collection 定時器的時機:
- 建立
Query
實例,呼叫setOptions(config.options)
更新gcTime
後 (L18-L20, L28) QueryObserver
向Query
取消訂閱,該Query
沒有其他QueryObserver
時 (L35-L38)- 當
Query
執行請求,收到失敗 (L48-L55),或成功回覆後 (L50-L61)
query.ts
export class Query<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Removable {
// ...
constructor(config: QueryConfig<TQueryFnData, TError, TData, TQueryKey>) {
super()
// ...
this.setOptions(config.options)
//...
this.scheduleGc()
}
setOptions(
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): void {
this.options = { ...this.#defaultOptions, ...options }
this.updateGcTime(this.options.gcTime)
}
// ...
removeObserver(observer: QueryObserver<any, any, any, any, any>): void {
if (this.observers.includes(observer)) {
this.observers = this.observers.filter((x) => x !== observer)
if (!this.observers.length) {
// ...
this.scheduleGc()
}
// ...
}
}
fetch(
options?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>,
fetchOptions?: FetchOptions<TQueryFnData>,
): Promise<TData> {
// ...
const onError = (error: TError | { silent?: boolean }) => {
// ...
if (!this.isFetchingOptimistic) {
// Schedule query gc after fetching
this.scheduleGc()
}
this.isFetchingOptimistic = false
}
// Try to fetch the data
this.#retryer = createRetryer({
// ...
onError,
onSuccess: (data) => {
// ...
if (!this.isFetchingOptimistic) {
// Schedule query gc after fetching
this.scheduleGc()
}
this.isFetchingOptimistic = false
},
// ...
})
return this.#retryer.start()
}
// ...
}
Mutation
在 query-core 中,Mutation
也是 Removable
的子類別,Mutation.optionalRemove()
則是向 MutationCahce
清除未使用的 Mutation
mutation.ts
export class Mutation<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
> extends Removable {
// ...
constructor(config: MutationConfig<TData, TError, TVariables, TContext>) {
super()
// ...
this.setOptions(config.options)
this.scheduleGc()
}
setOptions(
options: MutationOptions<TData, TError, TVariables, TContext>,
): void {
this.options = options
this.updateGcTime(this.options.gcTime)
}
protected optionalRemove() {
if (!this.#observers.length) {
if (this.state.status === 'pending') {
this.scheduleGc()
} else {
this.#mutationCache.remove(this)
}
}
}
// ...
}
Mutation
設置 garbage collection 定時器的時機:
- 建立
Mutation
實例,setOptions(config.options)
更新gcTime
之後 (L11-L12, L20) MutationObserver
向Mutation
取消訂閱時 (L23-L28)Mutation.scheduleGc()
定時器觸發後,Mutation.state.stauts === 'pending'
,重新設置定時器 (L32-L33)
mutation.ts
export class Mutation<
TData = unknown,
TError = DefaultError,
TVariables = unknown,
TContext = unknown,
> extends Removable {
// ...
constructor(config: MutationConfig<TData, TError, TVariables, TContext>) {
super()
// ...
this.setOptions(config.options)
this.scheduleGc()
}
setOptions(
options: MutationOptions<TData, TError, TVariables, TContext>,
): void {
this.options = options
this.updateGcTime(this.options.gcTime)
}
removeObserver(observer: MutationObserver<any, any, any, any>): void {
this.#observers = this.#observers.filter((x) => x !== observer)
this.scheduleGc()
// ...
}
protected optionalRemove() {
if (!this.#observers.length) {
if (this.state.status === 'pending') {
this.scheduleGc()
} else {
this.#mutationCache.remove(this)
}
}
}
// ...
}
Summary
- TanStack Query 是透過
setTimeout
定時檢查Query
,Mutation
實例的狀態,判斷是否需要向QueryCache
,MutationCache
清除該實例。gcTime
即為setTimeout
的 delay ms - 如果
Query
的請求進行中,或是有QueryObserver
訂閱中,該實例不會被清除 - 如果
Mutation
的請求進行中,或是有MutationObserver
訂閱中,該實例不會被清除 Query
,Mutation
會在不同時機,設置 garbage collection 定時器,例如:- 建立
Query
,Mutation
實例時 Query
執行請求,收到失敗或成功回覆時QueryObserver
向Query
取消訂閱,且該Query
沒有其他訂閱者時MutationObserver
向Mutation
取消訂閱時Mutation.scheduleGc()
定時器觸發後,該Mutation.state.status
仍為pending
時
- 建立