响应式

Vue 使用信号范例来处理和跟踪响应式。此系统的一个关键特性是响应式系统仅在专门监视的响应式属性上触发更新。这样做的结果是,您还需要确保在查询使用的值更新时更新查询。

保持查询的响应式

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

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
// 响应式用户 ID ref。
const userId = ref('1')
// 获取用户 1 的项目。
const { data: projects } = useUserProjects(userId.value)

const onChangeUser = (newUserId: string) => {
  // 编辑 userId,但查询不会重新获取。
  userId.value = newUserId
}
// 响应式用户 ID ref。
const userId = ref('1')
// 获取用户 1 的项目。
const { data: projects } = useUserProjects(userId.value)

const onChangeUser = (newUserId: string) => {
  // 编辑 userId,但查询不会重新获取。
  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) => {
  // 查询使用新的用户 ID 重新获取数据!
  userId.value = newUserId
}
const onChangeUser = (newUserId: string) => {
  // 查询使用新的用户 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
// 获取用户 1 的项目,userId 不会更改。
const { data: projects } = useUserProjects('1')

// 获取用户 1 的项目,查询将对 userId 的更改做出反应。
const userId = ref('1')

// 对 userId 进行一些更改...

// 查询根据对 userId 的任何更改重新获取。
const { data: projects } = useUserProjects(userId)
// 获取用户 1 的项目,userId 不会更改。
const { data: projects } = useUserProjects('1')

// 获取用户 1 的项目,查询将对 userId 的更改做出反应。
const userId = ref('1')

// 对 userId 进行一些更改...

// 查询根据对 userId 的任何更改重新获取。
const { data: projects } = useUserProjects(userId)

在查询内部使用派生状态

从另一个响应式状态源派生一些新的响应式状态是很常见的。通常,此问题出现在处理组件属性的情况下。假设我们的 userId 是传递给组件的属性:

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

您可能想直接在查询中使用该属性,如下所示:

ts
// 不会对 props.userId 的更改做出反应。
const { data: projects } = useUserProjects(props.userId)
// 不会对 props.userId 的更改做出反应。
const { data: projects } = useUserProjects(props.userId)

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

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

// 对 props.userId 的更改做出反应。
const { data: projects } = useUserProjects(userId)
const userId = computed(() => props.userId)

// 对 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
// 对 props.userId 的更改做出反应。不需要 `computed`!
const { data: projects } = useUserProjects(() => props.userId)
// 对 props.userId 的更改做出反应。不需要 `computed`!
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 之类的双向绑定将不起作用。在尝试就地更新数据之前,必须创建数据的可变副本。

关键要点

  • enabledqueryKey 是可以接受响应式值的两个查询选项。
  • 传递接受 Vue 中所有三种类型值的查询选项:ref、普通值和响应式 getter。
  • 如果您希望查询根据其使用的值的更改做出反应,请确保这些值是响应式的。(即直接将 ref 传递给查询,或使用响应式 getter)
  • 如果您不需要查询具有响应式,请传入一个普通值。
  • 对于诸如属性访问之类的简单派生状态,请考虑使用响应式 getter 代替 computed
  • useQuery 的结果始终是不可变的。