reactive
reactive 会对传入对象进行包裹,创建一个该对象的 Proxy 代理。它是源对象的响应式副本,不等于原始对象。它“深层”转换了源对象的所有嵌套 property,解包并维持其中的任何ref
引用关系。
reactive API
很好地解决了 Vue2 通过 defineProperty
实现数据响应式时的缺陷。使用也非常简单:
{{ state.age }}
当将 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 })
。
如何选择
ref
和reactive
?建议:
- 基础类型值(
String
、Number
、Boolean
等) 或单值对象(类似{ count: 3 }
这样只有一个属性值的对象) 使用ref
- 引用类型值(
Object
、Array
)使用reactive
- 对于 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}
再通过个小 对比下 ref
和 toRef
:
ref state1: {{ state1.count }}
toRef state2: {{ state2 }}
得出结论:
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
}
}
}
总结
ref
、reactive
是在setup()
声明组件内部状态用的, 这些变量通常都要 return 出去,除了供或渲染函数渲染视图,也可以作为 props 或 emit 参数 在组件间传递。它们的值变更可触发页面渲染。
而
toRef
、toRefs
主要用于处理 组件/
函数 传递的响应式数据,譬如在接收父组件props
时/
或composables
组合式函数返回数据时 建立起某些属性的响应式引用。通过
ref
包装的属性在 setup 函数内都需要通过.value
去访问它值 (template 模版内不用)。因此,ref
、toRef
创建的变量值都需要用变量.value
读取。reactive
则不用,因为会自动解包分配给它的ref
。
至于toRefs
,如果是解构赋值,如const { state1, state2 } = toRefs(props)
,值需要这样获取:state1.value.count
;
若整体赋给一个变量,如const state = toRefs(props)
,则是state.state1.value
。只有
toRefs
可以解构。以上四种方式声明的变量在通过
props
或 事件 传递时,均会维持其响应性。