#ref和reactive的区别
对比之前先看一下如何使用,它们的使用方法都很简单,也很类似:
<template> <div>{{user.first_name}} {{user.last_name}}</div> <div>{{ age }}</div> </template> <script> import { reactive } from 'vue' export default { setup() { const age = ref(18) const user = reactive({ first_name: "Karl", last_name: "Max", }) return { user , age} } } </script>
复制
#接下来我们就来分析一下它们的不同点:
1.可接受的原始数据类型不同
ref() 和reactive()都是接收一个普通的原始数据,再将其转换为响应式对象,例如上面代码中的user和age。却别在于:ref可以同时处理基本数据类型和对象,而reactive只能处理处理对象而支持基本数据类型。
const numberRef = ref(0); // OK const objectRef = ref({ count: 0 }) // OK //TS2345: Argument of type 'number' is not assignable to parameter of type 'object'. const numberReactive = reactive(0); const objectReactive = reactive({ count: 0}); // OK
复制
2.这是因为二者响应式数据实现的方式不同:
ref是通过一个中间对象RefImpl持有数据,并通过重写它的set和get方法实现数据劫持的,本质上依旧是通过Object.defineProperty 对RefImpl的value属性进行劫持。
reactive则是通过Proxy进行劫持的。Proxy无法对基本数据类型进行操作,进而导致reactive在面对基本数据类型时的束手无策。
ref对应的源码如下:
export function ref<T>(value: T): Ref<UnwrapRef<T>> export function ref<T = any>(): Ref<T | undefined> export function ref(value?: unknown) { return createRef(value, false) } function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) } class RefImpl<T> { private _value: T private _rawValue: T constructor(value: T, public readonly __v_isShallow: boolean) { this._rawValue = __v_isShallow ? value : toRaw(value) this._value = __v_isShallow ? value : toReactive(value) } /** * 重写get和set方法, * 本质上是通过Object.defineProperty * 对属性value进行劫持*/ get value() { //收集依赖 track() return this._value } set value(newVal) { if (hasChanged(newVal, this._rawValue)) { this._value = newVal //触发依赖 trigger() } } }
复制
删减整合后的reactive代码如下:
export function reactive(target: object) { return createReactiveObject( target, false ) } function createReactiveObject( target: Target, isReadonly: boolean ) { const proxy = new Proxy( target, { get(target, key) { //收集依赖 track() return target[propKey] }, set(target, key,value) { target[propKey] = value //触发依赖 trigger() } } ) return proxy }
复制
Object.defineProperty、Proxy和数据劫持的相关内容详情可见:Vue3数据劫持优化。
总结:ref可以存储基本数据类型而reactive则不能
返回值类型不同
运行如下代码:
const count1 = ref(0) const count2 = reactive({count:0}) console.log(count1) console.log(count2)
复制
输出结果为:
RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0} Proxy(Object) {count: 0}
复制
ref()返回的是一个持有原始数据的RefImpl实例。而reactive()返回的类型则是原始数据的代理Proxy实例
因此,在定义数据类型时,有些许差别:
interface Count { num:number } const countRef:Ref<number> = ref(0) const countReactive: Count = reactive({num:1})
复制
另外如果reactive中有响应式对象,它会被自动展开,所以下面代码是正确的:
const countReactiveRef: Count = reactive({num:ref(2)})
复制
结论:ref(value: T)返回的Ref 类型,而reactive(object: T)返回的T 类型的代理
访问数据的方式不同
返回值的类型不同,就会导致数据的访问方式不同。通过上文的可知:
ref()返回的是RefImpl的一个实例对象,该对象通过_value私有变量持有原始数据,并重写了value的get方法。因此,当想要访问原始对象的时候,需要通过xxx.value的方式触发get函数获取数据。同样的,在修改数据时,也要通过xxx.value = yyy的方式触发set函数。
reactive() 返回的是原始对象的代理,代理对象具有和原始对象相同的属性,因此我们可以直接通过.xxx的方式访问数据
反应在代码中如下:
const objectRef = ref({ count: 0 }); const refCount = objectRef.value.count; const objectReactive = reactive({ count: 0}); const reactiveCount = objectReactive.count;
复制
总结:ref需要通过value属性间接的访问数据(在templates中vue做了自动展开,可以省略.value),而reactive可以直接访问。
原始对象的可变性不同
ref通过一个RefImpl实例持有原始数据,进而使用.value属性访问和更新。而对于一个实例而言,其属性值是可以修改的。因此可以通过.value的方式为ref重新分配数据,无需担心RefImpl实例被改变进而破坏响应式:
const count = ref({count:1}) console.log(count.value.count) //修改原始值 count.value = {count:3} console.log(count.value.count) //修改原始值 count.value = {name:"Karl"} console.log(count.value.count) console.log(count.value.name) //输出如下: //1 //3 //undefined //karl
复制
而 reactive返回的是原始对象的代理,因此不能对其重新分配对象,只能通过属性访问修改属性值,否则会破坏掉响应式:
let objectReactive = reactive({ count: 0}) effect(() => { console.log(`数据变化了:${objectReactive.count}`) }) //可以正常修改值 objectReactive.count = 1 objectReactive.count = 2 //修改objectReactive之后effect不再会接收到数据变化的通知 objectReactive = {count:3} objectReactive.count = 4 console.log("结束了") //输出如下: //数据变化了:0 //数据变化了:1 //数据变化了:2 //结束了
复制
原因很简单:effect函数监听的是原始值{ count: 0}的代理objectReactive,此时当通过该代理修改数据时,可以触发回调。但是当程序运行到objectReactive = {count:3}之后,objectReactive 的指向不再是{ count: 0}的代理了,而是指向了新的对象{count:3}。这时objectReactive.count = 4修改的不再是effect 所监听的代理对象,而是新的普通的不具备响应式能力的对象{count:3}。effect就无法监听到数据的变化了,objectReactive响应式能力也因此而被破坏了。
如果你直接修改ref的指向,ref的响应式也会失效:
let count = ref(0) effect(() => { console.log(`数据变化了:${count.value}`) }) count.value = 1 count = ref(0) //effect不会监听到此处的变化 count.value = 2 console.log("结束了")
复制
结论:可以给ref的值重新分配给一个新对象,而reactive只能修改当前代理的属性
ref借助reactive实现对Object类型数据的深度监听
结合上文的RefImpl的源码:
constructor(value: T, public readonly __v_isShallow: boolean) { this._rawValue = __v_isShallow ? value : toRaw(value) this._value = __v_isShallow ? value : toReactive(value) } export const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value
复制
ref在发现被监听的原始对象是Object类形时,会将原始对象转换成reactive并赋值给_value属性。而此时ref.value返回的并不是原始对象,而是它的代理。
通过如下代码验证:
const refCount = ref({count:0}) console.log(refCount.value) //输出结果: //Proxy(Object) {count: 0}
复制
结论:``ref()在原始数据位Object类形时,会通过reactive包装原始数据后再赋值给_value。
对侦听属性的影响不同
执行如下代码:
let refCount = ref({count:0}) watch(refCount,() => { console.log(`refCount数据变化了`) }) refCount.value = {count:1} //输出结果: //refCount数据变化了
复制
watch()可以检测到ref.value的变化。然而,继续执行如下代码
let refCount = ref({count:0}) watch(refCount,() => { console.log(`refCount数据变化了`) }) refCount.value.count = 1 let reactiveCount = reactive({count:0}) watch(reactiveCount,() => { console.log(`reactiveCount数据变化了`) }) reactiveCount.count = 1 //输出结果 //reactiveCount数据变化了
复制
这次watch()没有监听到refCount的数据变化——watch()默认情况下不会深入观察 ref。若要watch深入观察ref,则需要修改参数如下:
watch(refCount, () => { console.log('reactiveCount数据变化了!') }, { deep: true })
复制
而对于reactive而言,无论你是否声明deep: true,watch都会深入观察。
结论:watch()默认情况下只监听ref.value的更改,而对reactive执行深度监听。
总结和用法
ref可以存储原始类型,而reactive不能。
ref需要通过<ref>.value访问数据,而reactive()可以直接用作常规对象。
可以重新分配一个全新的对象给ref的value属性,而reactive()不能。
ref类型为Ref<T>,而reactive返回的反应类型为原始类型本身。5516人阅读
前言
你一定知道Vue中的响应式编程,它提供了在数据变化时自动更新UI的能力,摒弃了传统的数据更新时手动更新UI的方式。在Vue 3.0之前,我们定义在data函数中的数据会被自动转换为响应式。而在 Composition API 中,还有两种方式让我们定义响应式对象:ref() 和reactive()。 但是,他们有什么不同之处呢?
ref和reactive的区别
对比之前先看一下如何使用,它们的使用方法都很简单,也很类似:
<template> <div>{{user.first_name}} {{user.last_name}}</div> <div>{{ age }}</div> </template> <script> import { reactive } from 'vue' export default { setup() { const age = ref(18) const user = reactive({ first_name: "Karl", last_name: "Max", }) return { user , age} } } </script>
复制
接下来我们就来分析一下它们的不同点:
可接受的原始数据类型不同
ref() 和reactive()都是接收一个普通的原始数据,再将其转换为响应式对象,例如上面代码中的user和age。却别在于:ref可以同时处理基本数据类型和对象,而reactive只能处理处理对象而支持基本数据类型。
const numberRef = ref(0); // OK const objectRef = ref({ count: 0 }) // OK //TS2345: Argument of type 'number' is not assignable to parameter of type 'object'. const numberReactive = reactive(0); const objectReactive = reactive({ count: 0}); // OK
复制
这是因为二者响应式数据实现的方式不同:
ref是通过一个中间对象RefImpl持有数据,并通过重写它的set和get方法实现数据劫持的,本质上依旧是通过Object.defineProperty 对RefImpl的value属性进行劫持。
reactive则是通过Proxy进行劫持的。Proxy无法对基本数据类型进行操作,进而导致reactive在面对基本数据类型时的束手无策。
ref对应的源码如下:
export function ref<T>(value: T): Ref<UnwrapRef<T>> export function ref<T = any>(): Ref<T | undefined> export function ref(value?: unknown) { return createRef(value, false) } function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) } class RefImpl<T> { private _value: T private _rawValue: T constructor(value: T, public readonly __v_isShallow: boolean) { this._rawValue = __v_isShallow ? value : toRaw(value) this._value = __v_isShallow ? value : toReactive(value) } /** * 重写get和set方法, * 本质上是通过Object.defineProperty * 对属性value进行劫持*/ get value() { //收集依赖 track() return this._value } set value(newVal) { if (hasChanged(newVal, this._rawValue)) { this._value = newVal //触发依赖 trigger() } } }
复制
删减整合后的reactive代码如下:
export function reactive(target: object) { return createReactiveObject( target, false ) } function createReactiveObject( target: Target, isReadonly: boolean ) { const proxy = new Proxy( target, { get(target, key) { //收集依赖 track() return target[propKey] }, set(target, key,value) { target[propKey] = value //触发依赖 trigger() } } ) return proxy }
复制
Object.defineProperty、Proxy和数据劫持的相关内容详情可见:Vue3数据劫持优化。
总结:ref可以存储基本数据类型而reactive则不能
返回值类型不同
运行如下代码:
const count1 = ref(0) const count2 = reactive({count:0}) console.log(count1) console.log(count2)
复制
输出结果为:
RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0} Proxy(Object) {count: 0}
复制
ref()返回的是一个持有原始数据的RefImpl实例。而reactive()返回的类型则是原始数据的代理Proxy实例
因此,在定义数据类型时,有些许差别:
interface Count { num:number } const countRef:Ref<number> = ref(0) const countReactive: Count = reactive({num:1})
复制
另外如果reactive中有响应式对象,它会被自动展开,所以下面代码是正确的:
const countReactiveRef: Count = reactive({num:ref(2)})
复制
结论:ref(value: T)返回的Ref 类型,而reactive(object: T)返回的T 类型的代理
访问数据的方式不同
返回值的类型不同,就会导致数据的访问方式不同。通过上文的可知:
ref()返回的是RefImpl的一个实例对象,该对象通过_value私有变量持有原始数据,并重写了value的get方法。因此,当想要访问原始对象的时候,需要通过xxx.value的方式触发get函数获取数据。同样的,在修改数据时,也要通过xxx.value = yyy的方式触发set函数。
reactive() 返回的是原始对象的代理,代理对象具有和原始对象相同的属性,因此我们可以直接通过.xxx的方式访问数据
反应在代码中如下:
const objectRef = ref({ count: 0 }); const refCount = objectRef.value.count; const objectReactive = reactive({ count: 0}); const reactiveCount = objectReactive.count;
复制
总结:ref需要通过value属性间接的访问数据(在templates中vue做了自动展开,可以省略.value),而reactive可以直接访问。
原始对象的可变性不同
ref通过一个RefImpl实例持有原始数据,进而使用.value属性访问和更新。而对于一个实例而言,其属性值是可以修改的。因此可以通过.value的方式为ref重新分配数据,无需担心RefImpl实例被改变进而破坏响应式:
const count = ref({count:1}) console.log(count.value.count) //修改原始值 count.value = {count:3} console.log(count.value.count) //修改原始值 count.value = {name:"Karl"} console.log(count.value.count) console.log(count.value.name) //输出如下: //1 //3 //undefined //karl
复制
而 reactive返回的是原始对象的代理,因此不能对其重新分配对象,只能通过属性访问修改属性值,否则会破坏掉响应式:
let objectReactive = reactive({ count: 0}) effect(() => { console.log(`数据变化了:${objectReactive.count}`) }) //可以正常修改值 objectReactive.count = 1 objectReactive.count = 2 //修改objectReactive之后effect不再会接收到数据变化的通知 objectReactive = {count:3} objectReactive.count = 4 console.log("结束了") //输出如下: //数据变化了:0 //数据变化了:1 //数据变化了:2 //结束了
复制
原因很简单:effect函数监听的是原始值{ count: 0}的代理objectReactive,此时当通过该代理修改数据时,可以触发回调。但是当程序运行到objectReactive = {count:3}之后,objectReactive 的指向不再是{ count: 0}的代理了,而是指向了新的对象{count:3}。这时objectReactive.count = 4修改的不再是effect 所监听的代理对象,而是新的普通的不具备响应式能力的对象{count:3}。effect就无法监听到数据的变化了,objectReactive响应式能力也因此而被破坏了。
如果你直接修改ref的指向,ref的响应式也会失效:
let count = ref(0) effect(() => { console.log(`数据变化了:${count.value}`) }) count.value = 1 count = ref(0) //effect不会监听到此处的变化 count.value = 2 console.log("结束了")
复制
结论:可以给ref的值重新分配给一个新对象,而reactive只能修改当前代理的属性
ref借助reactive实现对Object类型数据的深度监听
结合上文的RefImpl的源码:
constructor(value: T, public readonly __v_isShallow: boolean) { this._rawValue = __v_isShallow ? value : toRaw(value) this._value = __v_isShallow ? value : toReactive(value) } export const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value
复制
ref在发现被监听的原始对象是Object类形时,会将原始对象转换成reactive并赋值给_value属性。而此时ref.value返回的并不是原始对象,而是它的代理。
通过如下代码验证:
const refCount = ref({count:0}) console.log(refCount.value) //输出结果: //Proxy(Object) {count: 0}
复制
结论:``ref()在原始数据位Object类形时,会通过reactive包装原始数据后再赋值给_value。
对侦听属性的影响不同
执行如下代码:
let refCount = ref({count:0}) watch(refCount,() => { console.log(`refCount数据变化了`) }) refCount.value = {count:1} //输出结果: //refCount数据变化了
复制
watch()可以检测到ref.value的变化。然而,继续执行如下代码
let refCount = ref({count:0}) watch(refCount,() => { console.log(`refCount数据变化了`) }) refCount.value.count = 1 let reactiveCount = reactive({count:0}) watch(reactiveCount,() => { console.log(`reactiveCount数据变化了`) }) reactiveCount.count = 1 //输出结果 //reactiveCount数据变化了
复制
这次watch()没有监听到refCount的数据变化——watch()默认情况下不会深入观察 ref。若要watch深入观察ref,则需要修改参数如下:
watch(refCount, () => { console.log('reactiveCount数据变化了!') }, { deep: true })
复制
而对于reactive而言,无论你是否声明deep: true,watch都会深入观察。
结论:watch()默认情况下只监听ref.value的更改,而对reactive执行深度监听。
总结和用法
ref可以存储原始类型,而reactive不能。
ref需要通过<ref>.value访问数据,而reactive()可以直接用作常规对象。
可以重新分配一个全新的对象给ref的value属性,而reactive()不能。
ref类型为Ref<T>,而reactive返回的反应类型为原始类型本身。
watch默认只观察ref的value,而对reactive则执行深度监听。
ref默认会用reactive 对象类型的原始值进行深层响应转换。
使用习惯:虽然没有规则规定要在何时使用ref或者reactive,亦或是混合使用。这些完全取决于开发者的编程习惯。但是为了保持代码的一致性和可读性,我倾向于使用ref而非reactive。
vue.jsjavascript前端
来自专栏
Vue.js
著作权归作者所有
watch默认只观察ref的value,而对reactive则执行深度监听。
ref默认会用reactive 对象类型的原始值进行深层响应转换。
使用习惯:虽然没有规则规定要在何时使用ref或者reactive,亦或是混合使用。这些完全取决于开发者的编程习惯。但是为了保持代码的一致性和可读性,我倾向于使用ref而非reactive。