Vue3之弄清 ref、reactive、toRef、toRefs

1,154 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

reactive

reactive会对传入对象进行包裹,创建一个该对象的Proxy代理。它是源对象的响应式副本,不等于原始对象。它“深层”转换了源对象的所有嵌套 property,解包并维持其中的任何ref引用关系。

reactive API很好地解决了 Vue2 通过 defineProperty 实现数据响应式时的缺陷。使用也非常简单:

<template>
  <div>
    <!-- 4. 访问响应式数据对象中的 age  -->
    {{ state.age }}
  </div>
</template>

<script>
// 1. 从 vue 中导入 reactive 
import { reactive } from 'vue'
export default {
  name: 'User',
  setup() {
    // 2. 创建响应式的数据对象
    const user = reactive({ age: 33 })
    // 3. 将响应式数据对象 user return 出去,供 template 使用
    return { user }
  }
}
</script>

当将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 方法使该对象具有高度的响应式。

<script>
import { ref } from 'vue'
export default {
  name: 'User',
  setup() {
    const user = ref({ age: 30 })
    const userJob = ref('前端工程师')
    console.log(user.value) // { age: 30 }
    console.log(userJob.value) // '前端工程师'
    console.log(user) // RefImpl {...}
  }
}
</script>

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

const foo = ref<string | number>('foo') // foo 的类型:Ref<string | number>
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

<template>
  <p>ref state1: {{ state1.count }}</p>
  <button @click="add1">增加 state1 的 count</button>
  <p>toRef state2: {{ state2 }}</p>
  <button @click="add2">增加 state2</button>
</template>

<script>
import { ref, toRef } from 'vue'
export default {
  setup() {
    const obj = { count: 3 }
    const state1 = ref(obj) 
    // const state1 = ref(obj.count) // 如果传简单数据类型是值传递,无法体现对源数据的响应式
    const state2 = toRef(obj, 'count')
    function add1() {
      state1.value.count ++
      console.log('原始值:', obj); // obj的 count 递增
      console.log('响应式数据对象:', state1.value.count); 
      console.log('state2', state2.value)
      // state1 的 count 属性 和 state 2 递增,页面上的 state1 和 state2 也递增
    }
    function add2() {
      state2.value ++
      console.log('原始值:', obj); // obj 的 count 递增
      console.log('响应式数据对象:', state2.value); 
      console.log('state1', state1.value.count)
      // state2 的值 和 state1 的 count 属性也同步递增,页面上的 state1 和 state2无变化
    }

    return { state1, state2, add1, add2 }
  }
}
</script>

得出结论:
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 出去,除了供<template>或渲染函数渲染视图,也可以作为 propsemit 参数在组件间传递。它们的值变更可触发页面渲染

  • toReftoRefs主要用于处理 组件/函数 传递的响应式数据,譬如在接收父组件props/composables组合式函数返回数据时 建立起某些属性的响应式引用

  • 通过ref包装的属性在 setup 函数内都需要通过.value去访问它值 (template 模版内不用)。因此,reftoRef创建的变量值都需要用变量.value读取。reactive则不用,因为会自动解包分配给它的ref

    至于**toRefs**,如果是解构赋值,如const { state1, state2 } = toRefs(props),值需要这样获取:state1.value.count

    若整体赋给一个变量,如const state = toRefs(props),则是state.state1.value

  • 只有toRefs可以解构。

  • 以上四种方式声明的变量在通过 props 或 事件 传递时,均会维持其响应性