vue3 ref全家桶

155 阅读6分钟

ref

源码位置:packages/reactivity/src/ref.ts

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。若要避免这种深层次的转换,请使用 shallowRef() 来替代

ref的使用

ref定义变量:Ref 可以持有任何类型的值,包括基本数据类型、深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到:

import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

const obj = ref({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // 以下都会按照期望工作
  obj.value.nested.count++
  obj.value.arr.push('baz')
}

在模板中使用: 在模板中使用 ref 时,需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包。你也可以直接在事件监听器中改变一个 ref:

<button @click="count++">
  {{ count }}
</button>

ref的注意事项

1. 作为reactive对象的属性

一个ref在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性。

const count = ref(0)
const state = reactive({
    count
})
console.log(state.count) // 0,打印出来的是0 而不是Ref<0>
state.count++
console.log('count', count.value) // 1

2. 数组和集合

与 reactive 对象不同的是,当 ref 作为响应式数组原生集合类型(如 Map) 中的元素被访问时,它不会被解包:

  const books = reactive([ref('Vue 3 Guide')])
  // 这里需要 .value
  console.log(books[0].value)

  const books1 = ref([ref('Vue 3 Guide')])
  // 这里需要 .value
  console.log('books1 ref:', books1.value[0].value)

  const map = reactive(new Map([['count', ref(0)]]))
  // 这里需要 .value
  console.log(map.get('count')?.value)

3. 在模板中解包

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

const count = ref(0)
<div>{{ count++ }}</div>

上述例子正常工作。

const object = {
  id: ref(1)
}
<div>{{ object.id + 1 }}</div>

在vscode会有如下错误提示:

image.png 渲染的结果为[object Object]1,因为在计算object.id时未解包。解决方案为:

const { id } = object
<div>{{ id + 1 }}</div>
// 或者
<div>{{ object.id.value + 1 }}</div>

特殊情况: 如果 ref 是文本插值的最终计算值 (即 {{ }} 标签),那么它将被解包,因此以下内容将渲染为 1

<div>{{ object.id }}</div>

为ref()标注类型

1. 根据初始化时的值推导其类型

const str = ref('hello') // 类型推断为Ref<string>
const number = ref(1)    // 类型推断为Ref<number>
const obj = ref({        // 类型推断为Ref<{name: string}>
    name: 'kk'
})

2. 通过Ref这个类型

import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')

year.value = 2020 // 成功

3. 在调用ref时,传入一个泛型参数,来覆盖默认的推导行为

const year = ref<string | number>('2020')

year.value = 2020 // 成功

// 类型推断为`Ref<string | number | undefined>`
const year2 = ref<string | number>() 

isRef

判断某个值是否为ref

function isRef<T>(r: Ref<T> | unknown): r is Ref<T>

返回值是一个类型判定 (type predicate),这意味着 isRef 可以被用作类型守卫:

let foo: unknown
if (isRef(foo)) {
  // foo 的类型被收窄为了 Ref<unknown>
  foo.value
}

unref

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

function unref<T>(ref: T | Ref<T>): T
function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x)
  // unwrapped 现在保证为 number 类型
}

toRef

可以将值、refs 或 getters 规范化为 refs (3.3+)。

也可以基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

规范化签名:

// 按原样返回现有的 ref
toRef(existingRef)

// 创建一个只读的 ref,当访问 .value 时会调用此 getter 函数
toRef(() => props.foo)

// 从非函数的值中创建普通的 ref
// 等同于 ref(1)
toRef(1)

用在对象的属性上:

const state = reactive({
  foo: 1,
  bar: 2
})

// 双向 ref,会与源属性同步
const fooRef = toRef(state, 'foo')

// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2

// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3

当 toRef 与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。尝试将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。在这种场景下,你可能可以考虑使用带有 get 和 set 的 computed 替代

toRefs

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

类型:

function toRefs<T extends object>(
  object: T
): {
  [K in keyof T]: ToRef<T[K]>
}

type ToRef = T extends Ref ? T : Ref<T>
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// 这个 ref 和源属性已经“链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

当从组合式函数中返回响应式对象时,toRefs 相当有用。使用它,消费者组件可以解构/展开返回的对象而不会失去响应性:

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // ...基于状态的操作逻辑

  // 在返回时都转为 ref
  return toRefs(state)
}

// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX()
<div>foo:{{ foo }}--bar:{{ bar }}</div>
  • toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef

shallowRef

和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

triggerRef

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。

const shallow = shallowRef({
  greet: 'Hello, world'
})

// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 打印 "Hello, universe"
triggerRef(shallow)

customRef

创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。 类型:

function customRef<T>(factory: CustomRefFactory<T>): Ref<T>

type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

说明:customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。

一般来说,track() 应该在 get() 方法中调用,而 trigger() 应该在 set() 中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。

示例:创建一个防抖ref,只有在set之后固定的一段时间内才更新值

import { customRef } from 'vue'

export function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

在组件中使用:

<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>

<template>
  <div>{{ text }}</div>
  <input v-model="text" />
</template>