Vue 3 之:弄清 ref reactive toRef toRefs

reactive

reactive 会对传入对象进行包裹,创建一个该对象的 Proxy 代理。它是源对象的响应式副本,不等于原始对象。它“深层”转换了源对象的所有嵌套 property,解包并维持其中的任何ref引用关系。
reactive API很好地解决了 Vue2 通过 defineProperty 实现数据响应式时的缺陷。使用也非常简单:




当将 ref 分配给 reactive property 时,ref 将被自动解包,无需再用.value访问。

import { ref, reactive } from 'vue'
export default {
  setup() {
    const num1 = ref(1)
    const num2 = ref(2)
    const obj = reactive({ num1 })

    // 把 ref 分配给 reactive property,ref 将被自动解包。
    obj.num2 = num2
    console.log(obj.num2) // 2
    console.log(obj.num2 === num2.value) // true

    // ref 会被解包
    console.log(obj.num1 === num1.value) // true

    // 修改 `num1` ref 的值会更新 `obj.num1`的值
    num1.value++
    console.log(num1.value) // 2
    console.log(obj.num1) // 2

    // 修改 obj 的 num1 property 也会更新 `num1` ref
    obj.num1++
    console.log(obj.num1) // 3
    console.log(num1.value) // 3

    console.log(obj) // Proxy {num1: RefImpl, num2: RefImpl}
  }
}

ref

ref 函数用来将一项数据包装成一个响应式 ref 对象。它接收任意数据类型的参数,作为这个 ref 对象 内部的 value property 的值。之后可以用ref对象.value访问或更改这个值。
因为基础数据类型只能传递值而不是引用地址,将它包装在一个对象内,可以实现数据的响应式。

可以通过 isRef 判断变量是否是 Ref 对象。
如果将对象分配为 ref 值,则内部会通过 reactive 方法使该对象具有高度的响应式。


有时我们可能需要为 ref 的内部值指定复杂类型。想要简洁地做到这一点,我们可以在调用 ref 覆盖默认推断时传递一个泛型参数:

const foo = ref('foo') // foo 的类型:Ref
foo.value = 123 // ok!

因为ref就是通过reactive包装了一个对象 ,然后将值传给该对象的value属性,这也就是为什么每次访问时我们都需要加上.value。可以简单地把 ref(obj) 理解为 reactive({ value: obj })

如何选择 refreactive?建议:

  1. 基础类型值(StringNumberBoolean等) 或单值对象(类似{ count: 3 }这样只有一个属性值的对象) 使用 ref
  2. 引用类型值(ObjectArray)使用 reactive
  3. 对于 ref 对象可以使用 unref 语法糖来免去.value访问的困扰

toRef

toRef 函数可以为传入对象的某个属性新创建一个响应式引用 ref。这个 ref 可以被传递,它会保持对其源 property 的响应式连接。
第一个参数为源对象,第二个参数为源对象中的属性名。

const state = reactive({
  foo: 1,
})
const fooRef = toRef(state, 'foo') // 和 state 的 foo 属性建立了高度响应式连接

fooRef.value++
console.log(state.foo) // 2
// 原 Proxy 对象 state 的 foo 被影响了

state.foo++
console.log(fooRef.value) // 3
// 同步修改了 fooRef 的值

console.log(fooRef) // ObjectRefImpl {_object: Proxy, _key: "foo", __v_isRef: true}

再通过个小 对比下 reftoRef




得出结论:
ref 创建一个响应式对象,如果传入参数是对象,那么与对象所有嵌套属性都维持数据响应。它的作用是 data 选项 般的存在,即组件内部状态ref值改变会触发页面渲染,同时能作为props或 事件参数 进行组件通信。
toRef 是对传入对象指定属性的响应式绑定值改变不会更新视图。因此用它创建的变量不作用于模版渲染,而是用来接收诸如props引用式传递

当你要将 prop 的某个ref(即用 ref 包装的属性) 传递给复合函数时,toRef 很有用:

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

即使源 property 不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用,因为可选 prop 并不会被 toRefs 处理。

toRefs

了解完 toRef 后,就很好理解 toRefs 了,其作用是生成一个新对象,内部每个属性都指向传入的对象的相应 property 的响应式数据 ref
也就是说,新对象本身与原对象的无关联(指向新的引用地址),但它的所有属性却都与源对象的对应属性建立了响应性。
toRef 可以记成建立一个 ref 属性值的引用,toRefs 则是所有 ref 响应属性值的引用。

看下 :
如果对响应式对象进行解构,被解构的两个 property 的响应性都会丢失。

const book = reactive({
  author: 'Vue Team',
  year: '2020',
  title: 'Vue 3 Guide',
  description: 'You are reading this book right now ;)',
  price: 'free'
})

let { author, title } = book

可以用toRefs函数建立起与源对象的响应式关联:

const book = reactive({ ... })
let { author, title } = toRefs(book)

title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
book.author = 'Jay Chou'
console.log(author.value) // 'Jay Chou'

实际开发中,比如需要解构 props,就可以这样操作来保证与 props 参数的响应式引用:

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)
  console.log(title.value)
}

但如果 title 是可选 prop,则传入的 props 中可能没有 title 。这种情况下,toRefs 将不会为 title 创建一个 ref ,此时就需要用 toRef 替代它:

import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

像上面这样做确保我们的侦听器能够根据 title prop 的变化做出反应

当从组合式函数返回响应式对象时, toRefs非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:

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

  // ... 操作 state 的逻辑

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

export default {
  setup() {
    // 可以在不失去响应性的情况下解构
    const { foo, bar } = useFeatureX()
    return {
      foo,
      bar
    }
  }
}

总结

  • refreactive是在setup()声明组件内部状态用的, 这些变量通常都要 return 出去,除了供