响应性

Vue 使用 Signals 范式 来处理和跟踪响应性。此系统的关键特性是响应式系统仅在特定观察到的响应式属性上触发更新。由此产生的后果是,您还需要确保在查询所消耗的值更新时,查询也会随之更新。

保持查询的响应性

在为查询创建组合函数时,您的首选可能是这样编写:

ts
export function useUserProjects(userId: string) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(userId),
  );
}
export function useUserProjects(userId: string) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(userId),
  );
}

我们可能会这样使用此组合函数:

ts
// Reactive user ID ref.
const userId = ref('1')
// Fetches the user 1's projects.
const { data: projects } = useUserProjects(userId.value)

const onChangeUser = (newUserId: string) => {
  // Edits the userId, but the query will not re-fetch.
  userId.value = newUserId
}
// Reactive user ID ref.
const userId = ref('1')
// Fetches the user 1's projects.
const { data: projects } = useUserProjects(userId.value)

const onChangeUser = (newUserId: string) => {
  // Edits the userId, but the query will not re-fetch.
  userId.value = newUserId
}

此代码将无法按预期工作。这是因为我们直接从中提取了 `userId` ref 的值。Vue-query 没有跟踪 `userId` ref,因此它无法知道何时值会发生变化。

幸运的是,这个问题的修复很简单。该值必须在查询键中进行跟踪。我们只需在组合函数中直接接受 `ref` 并将其放入查询键中。

ts
export function useUserProjects(userId: Ref<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(userId.value),
  );
}
export function useUserProjects(userId: Ref<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(userId.value),
  );
}

现在,当 `userId` 更改时,查询将重新获取数据。

ts
const onChangeUser = (newUserId: string) => {
  // Query refetches data with new user ID!
  userId.value = newUserId
}
const onChangeUser = (newUserId: string) => {
  // Query refetches data with new user ID!
  userId.value = newUserId
}

在 Vue Query 中,查询键内的任何响应式属性都会被自动跟踪更改。这允许 Vue Query 在给定请求的参数发生变化时重新获取数据。

考虑非响应性查询

虽然可能性大大降低,但有时传递非响应式变量是故意的。例如,某些实体只需要获取一次,不需要跟踪,或者我们在突变后使突变查询选项对象失效。如果我们使用上面定义的自定义组合函数,在这种情况下使用起来感觉有点不对劲。

ts
const { data: projects } = useUserProjects(ref('1'))
const { data: projects } = useUserProjects(ref('1'))

我们必须创建一个中间 `ref` 才能使参数类型兼容。我们可以在这里做得更好。让我们将组合函数更新为同时接受普通值和响应式值。

ts
export function useUserProjects(userId: MaybeRef<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(toValue(userId)),
  );
}
export function useUserProjects(userId: MaybeRef<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(toValue(userId)),
  );
}

现在我们可以将组合函数与普通值和 ref 一起使用。

ts
// Fetches the user 1's projects, userId is not expected to change.
const { data: projects } = useUserProjects('1')

// Fetches the user 1's projects, queries will react to changes on userId.
const userId = ref('1')

// Make some changes to userId...

// Query re-fetches based on any changes to userId.
const { data: projects } = useUserProjects(userId)
// Fetches the user 1's projects, userId is not expected to change.
const { data: projects } = useUserProjects('1')

// Fetches the user 1's projects, queries will react to changes on userId.
const userId = ref('1')

// Make some changes to userId...

// Query re-fetches based on any changes to userId.
const { data: projects } = useUserProjects(userId)

在查询中使用派生状态

从一个响应式状态派生出新的响应式状态是很常见的。通常,这个问题会出现在处理组件 props 的情况下。假设我们的 `userId` 是传递给组件的 prop。

vue
<script setup lang="ts">
const props = defineProps<{
  userId: string
}>()
</script>
<script setup lang="ts">
const props = defineProps<{
  userId: string
}>()
</script>

您可能会尝试像这样直接在查询中使用 prop:

ts
// Won't react to changes in props.userId.
const { data: projects } = useUserProjects(props.userId)
// Won't react to changes in props.userId.
const { data: projects } = useUserProjects(props.userId)

然而,与第一个示例类似,这也不是响应式的。对 `reactive` 变量的属性访问会导致响应性丢失。我们可以通过 `computed` 使此派生状态具有响应性来修复此问题。

ts
const userId = computed(() => props.userId)

// Reacts to changes in props.userId.
const { data: projects } = useUserProjects(userId)
const userId = computed(() => props.userId)

// Reacts to changes in props.userId.
const { data: projects } = useUserProjects(userId)

这可以按预期工作,但是,这个解决方案并不总是最理想的。除了引入中间变量之外,我们还创建了一个有点不必要的记忆化值。对于简单的属性访问的琐碎情况,`computed` 是一个没有真正优势的优化。在这些情况下,一个更合适的解决方案是使用 响应式 getter。响应式 getter 仅仅是根据一些响应式状态返回值的函数,这与 `computed` 的工作方式类似。与 `computed` 不同,响应式 getter 不会对其值进行记忆化,因此它成为简单属性访问的一个不错的选择。

让我们再次重构我们的组合函数,但这次我们将使其接受一个 `ref`、普通值或响应式 getter。

ts
export function useUserProjects(userId: MaybeRefOrGetter<string>) {
  ...
}
export function useUserProjects(userId: MaybeRefOrGetter<string>) {
  ...
}

让我们调整我们的用法,现在使用响应式 getter。

ts
// Reacts to changes in props.userId. No `computed` needed!
const { data: projects } = useUserProjects(() => props.userId)
// Reacts to changes in props.userId. No `computed` needed!
const { data: projects } = useUserProjects(() => props.userId)

这为我们提供了简洁的语法和所需的响应性,而没有任何不必要的记忆化开销。

其他跟踪的查询选项

上面,我们只触及了跟踪响应式依赖的一个查询选项。但是,除了 `queryKey` 之外,`enabled` 也允许使用响应式值。在您想根据派生状态控制查询的获取时,这会派上用场。

ts
export function useUserProjects(userId: MaybeRef<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(toValue(userId)),
    enabled: () => userId.value === activeUserId.value,
  );
}
export function useUserProjects(userId: MaybeRef<string>) {
  return useQuery(
    queryKey: ['userProjects', userId],
    queryFn: () => api.fetchUserProjects(toValue(userId)),
    enabled: () => userId.value === activeUserId.value,
  );
}

有关此选项的更多详细信息,请参阅 useQuery 参考页面。

不变性

`useQuery` 的结果始终是不可变的。这对于性能和缓存是必需的。如果您需要更改 `useQuery` 返回的值,则必须创建数据的副本。

此设计的一个含义是,将 `useQuery` 的值传递给双向绑定(例如 `v-model`)将不起作用。在尝试就地更新数据之前,必须创建数据的可变副本。

关键要点

  • `enabled` 和 `queryKey` 是可以接受响应式值的两个查询选项。
  • 在 Vue 中,将接受所有三种类型值的查询选项:ref、普通值和响应式 getter。
  • 如果您期望查询响应它所消耗的值的变化,请确保这些值是响应式的。(即直接将 ref 传递给查询,或使用响应式 getter)。
  • 如果您不需要查询具有响应性,请传入一个普通值。
  • 对于琐碎的派生状态,例如属性访问,请考虑使用响应式 getter 代替 `computed`。
  • `useQuery` 的结果始终是不可变的。