引言
ref 和 reactive 是 Vue 3 的 Composition API 中提供的两个响应式数据声明方法,它们都是 Vue 3 中实现响应式数据的关键手段。然而,它们在使用方式和适用场景上有所不同。
本文将会比较他们的使用方式、优缺点和适用场景,并对原理进行分析。
ref
ref 是一个函数,它的作用是将一个普通的 JavaScript 变量(基本类型或者引用类型)包装成一个响应式的数据。 官方文档给出的解释是:接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
看下下面这个使用 ref 的例子:
<template>
<div class="main">
<p>price: {{ product.price }}</p>
<button @click="changeProductPrice">修改product.price</button>
<p>price: {{ count }}</p>
<button @click="changeProductPrice">修改count</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const product = ref({ price: 0 })
const count = ref(0)
const changeProductPrice = () => {
product.value.price += 10
}
const changeCount = () => {
count.value += 1
}
</script>上面这段代码中,我们首先使用 ref 函数创建了一个名为 count 的响应式引用,初始值为基本数据类型。 然后使用 ref 函数创建了一个名为 product 的响应式引用,初始值为引用数据类型,是一个对象。 可以看到,更改这两个响应式引用的方法略有不同。
ref 函数的参数既可以传递原始数据类型也可以传递引用类型,但需要注意的是
- 如果传递的是原始数据类型的值,那么指向原始数据的那个值保存在返回的响应式数据的
.value中,例如上面的count.value; - 如果传递的一个引用类型的值,例如传个对象,返回的响应式数据的
.value中对应有指向原始数据的属性,例如上面的product.value.price
我们不妨打印一下 count 和 product 这两个响应式数据,看看有什么不一样的地方:

上图中,我们可以看到:不管给 ref 函数传递原始数据类型的值还是引用数据类型的值,返回的都是由 RefImpl 类构造出来的对象,但不同的是对象里面的 value:
- 如果
ref函数参数传递的是原始数据类型的值,那么value是一个原始值 - 如果
ref函数参数传递的是引用数据类型的值,那么value是一个Proxy对象
ref 的解包
在模板中解包
TIP
解包指的是 ref 在模板上下文即 template 中会被自动解析出来,使用时不需要加.value
解包的规则是:只有最顶级的 ref 属性才会被解包。
是否是最顶级主要是判断 ref 是直接定义的变量还是作为对象的属性。比如在下面的例子中,count 是最顶级变量,但是 person.age 不是。 所以count 会被自动解包,而 person.age 不会自动解包。
<template>
<div class="main">
<p>count: {{ count + 1 }}</p>
<p>age: {{ person.age + 1 }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const person = {
age: ref(26),
}
</script>对于count,页面直接渲染出来结果 2 ,但是对于 person.age ,页面渲染的结果却是:[object Object]1。这是因为在计算表达式时 person.age 没有被解包,仍然是一个 ref 对象。
为了解决这个问题,我们可以将 age 解构成为一个顶级属性:
<template>
<div class="main">
age input:
<input type="text" v-model="age" />
<p>age: {{ age }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const person = {
age: ref(26),
}
const { age } = person
</script>可以看到: age 经过结构之后,就可以成功解包并保持其响应性。
在 reactive 中的解构
ref 作为 reactive 的属性被访问或者修改时会被自动解包。
这个很好理解,我们直接看下面的代码就行:
<template>
<div class="main">
<p>count: {{ state.count }}</p>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0)
const state = reactive({
count,
})
console.log(state.count === count.value) // true
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
</script>优点
- 统一性
ref 的核心优势之一是它的统一性。它提供了一种简单、一致的方式来处理所有类型的数据,无论是数字、字符串、对象还是数组。这种统一性极大地简化了开发者的代码,减少了在不同数据类型之间切换时的复杂性。
import { ref } from 'vue'
const num = ref(0)
const str = ref('Hello')
const obj = ref({ count: 0 })
// 修改基本数据类型
num.value++
str.value += ' World'
// 修改对象
obj.value.count++- 深层响应性
ref 支持深层响应性,这意味着它可以追踪和更新嵌套对象和数组中的变化。这种特性使得 ref 非常适合处理复杂的数据结构,如对象和数组。
import { ref } from 'vue'
const obj = ref({
user: {
name: 'xiaoming',
details: {
age: 18,
},
},
})
// 修改嵌套对象,模版中的 age 会自动更新
obj.value.user.details.age++当然,为了减少大型不可变数据的响应式开销,也可以通过使用 shallowRef 来放弃深层响应性。
import { shallowRef } from 'vue'
const obj = shallowRef({
user: {
name: 'xiaoming',
details: {
age: 18,
},
},
})
obj.value.user.details.age++
// 修改嵌套对象,模版中的 age 不会自动更新
obj.value = {
user: {
name: 'xiaoming',
details: {
age: 20,
},
},
}
// 只有整个替换掉.value 对象时,才会触发模板更新- 灵活性
ref 提供了高度的灵活性,尤其在处理普通赋值和解构赋值方面。这种灵活性使得 ref 在开发中的使用更加方便,特别是在进行复杂的数据操作时。
import { ref } from 'vue'
const state = ref({
count: 0,
name: 'Vue',
})
// 解构赋值
const { count, name } = state.value
// 直接修改解构后的变量
count++
name = 'Vue3'
// 替换整个对象
state.value = {
count: 10,
name: 'Vue4',
}- 不易丢失响应
ref 响应式还是比较稳定的,无论是直接结构还是传递给函数结构赋值都不会丢失响应性。这里就不再举例了,可以参考上面几个例子。
缺点
- 到处
.value属实有点麻烦
给ref赋值或者取值时必须加 .value,有点麻烦,我们可以通过更改 vscode 的配置项去让编辑器自动的为我们补全 ref 。
点击 vscode 中左下角 齿轮 -> 设置 -> 搜索 Dot Value
找到下面的选项,打勾:

reactive
reactive 也是一个函数,它的作用是将一个普通的对象转换成响应式对象。它会递归地将对象的所有属性转换为响应式数据。返回一个 Proxy 对象。
它的特点是适用于创建复杂对象的响应式数据;同时,reactive 对象在模板中不会自动解构,需要通过对象属性访问。
reactive 的参数只能是对象或者数组或者像 Map 、 Set 这样的集合类型。
<script setup>
import { reactive } from 'vue'
// 使用 reactive 创建一个包含多个响应式属性的对象
const person = reactive({
name: 'Echo',
age: 25,
})
console.log(person.name) // 读取属性值:'Echo'
person.age = 28 // 修改属性值
console.log(person.age) // 读取修改后的属性值:28
</script>下面我们在控制台中打印一下 person 对象,看是什么东西:

可以看到,打印出来的是一个 Proxy 对象,也就是说:reactive 实现响应式是基于 ES6 Proxy 实现的。具体细节可以在 reactive-源码解读 这一节中看到。
注意点
- reactive() 返回的是一个原始对象的 Proxy,它和原始的对象是不相等的
<script setup>
import { reactive } from 'vue'
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
</script>- 当原始对象里面的数据发生改变时,会影响代理对象;代理对象里面的数据发生变化时,对应的原始数据也会发生变化
<script setup>
import { reactive } from 'vue'
const obj = {
count: 1,
}
const proxy = reactive(obj)
proxy.count++
console.log(proxy.count) // 2
console.log(obj.count) // 2
</script>那么问题来了,当原始对象里面的数据发生改变时,会影响代理对象;代理对象里面的数据发生变化时,对应的原始数据也会发生变化,那么会引出一个疑问——在我们实际开发中应该操作原始对象还是代码对象?
答案是:代理对象。因为只有代理对象是响应式的,更改原始对象虽然在 js中与更改代理对象表现一致,但是在 template中却不会触发更新。看下面代码:
<template>
<div class="main">
obj.count input 无代理:
<input type="text" v-model="obj.count" />
proxy.count input 有代理:
<input type="text" v-model="proxy.count" />
<p>obj.count 值:{{ obj.count }}</p>
<p>proxy.count 值:{{ proxy.count }}</p>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const obj = {
count: 1,
}
const proxy = reactive(obj)
</script>我们可以看到,当在第一个 input(无代理)更改数据时,count 没有获得响应式更新,obj.count 和 proxy.count 都没有改变数值,但是当在第二个 input(有代理)更改数据时,count 获得了响应式更新,obj.count 和 proxy.count 都改变了数值。
- 为保证访问代理的一致性,对同一个原始对象调用
reactive()会总是返回同样的代理对象,而对一个已存在的代理对象调用reactive()会返回其本身
<script setup>
import { reactive } from 'vue'
const raw = {}
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)
console.log(proxy1 === proxy2) // true
console.log(reactive(proxy1) === proxy1) // true
</script>下面总结下 reactive 的优缺点:
优点
- 使用 reactive 定义的响应式对象,会深度监听每一层的属性,它会影响到所有嵌套的属性,也就是说,对象的每一层都是具有响应性的
<script setup>
import { reactive } from 'vue'
let obj = reactive({
name: 'Echo',
a: {
b: {
c: 1,
},
},
})
console.log('obj: ', obj)
console.log('obj.name: ', obj.name)
console.log('obj.a: ', obj.a)
console.log('obj.a.b: ', obj.a.b)
console.log('obj.a.b.c: ', obj.a.b.c)
</script>
我们可以看到,返回的对象以及其中嵌套的对象都会通过 Proxy 包裹。因此每层对象都会保持其响应性。
若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,我们可以使用shallowReactive。
<script setup>
import { shallowReactive } from 'vue'
let obj = shallowReactive({
name: 'Echo',
a: {
b: {
c: 1,
},
},
})
</script>
<template>
<div class="main">
obj.name:
<input type="text" v-model="obj.name" />
obj.a.b.c:
<input type="text" v-model="obj.a.b.c" />
<p>obj.name: {{ obj.name }}</p>
<p>obj.a.b.c: {{ obj.a.b.c }}</p>
</div>
</template>
可以看到:只有更改对象自身属性,也就是 name 时,响应式才存在,更改内部属性 obj.a.b.c 时,响应式就不存在了。
缺点
- reactive 的参数只能是对象或者数组或者像 Map、Set 这样的集合类型
<script setup>
import { reactive } from 'vue'
// 对于基本数据类型无效
let count = reactive(0)
</script>
- 当我们将响应式对象的原始类型属性进行解构时,会丢失响应式
<script setup>
const state = reactive({ count: 0 })
let { count } = state
// 不会影响原始的 state
count++
// 当解构时,count 就已经与 state.count 断开连接,所以这里我们即使count++,模板中的count 值不会改变
fn(count) // 函数直接使用结构出的count值,无响应性
fn(state) // 函数使用state,使用state.count,有响应性
</script>为了让 reactive 结构出的值也具有响应性,解决办法是使用 toRefs 将 reactive 中的变量转化为响应式:
import { toRefs } from 'vue'
const state = reactive({ count: 0 })
let { count } = toRefs(state)
count++ // count 现在是 1,同时模板中的count也具有响应性- 重新赋值时,会丢失响应式
无论是将reactive 对象赋值给一个普通对象或是另一个reactive 对象 ,都会导致其丢失响应式。
import { reactive } from 'vue'
const state = reactive({ count: 0 })
state = { count: 2 } // 失去响应性
state = reactive({ count: 1 }) // 失去响应性解决办法是有两种:
不要将整个对象替换,一个个属性去赋值
jslet state = reactive({ count: 0 }) state.count = 1但是这种方法属性多了或者是需要批量去赋值的时候就不好办了,可以用下面这个方法
使用
Object.assign()
let state = reactive({ count: 0 })
state = Object.assign(state, { count: 1 })为什么使用 Object.assign()就可以
reactive 重新赋值丢失响应是因为引用地址变了,被 proxy 代理的对象已经不是原来的那个对象,所以丢失响应了。 而 Object.assign解释是这样的:如果目标对象与源对象具有相同的键(属性名),则目标对象中的属性将被源对象中的属性覆盖。这个操作只会影响原对象的属性值,是不会改变原对象的引用地址的。所以当 Object.assign(state, { count: 1 }) 时,所以只要 proxy 代理的引用地址没变,原对象state就会一直保持响应性。
- 当我们将响应式对象的属性赋值给变量时,会丢失响应式
const state = reactive({ count: 0 })
let count = state.count
count++ // 响应式会丢失这种操作会丢失响应性是因为: Proxy 代理的范围是对象,不代理对象属性的值,当把 state.count 赋值给 count 时,其实相当于基本类型的值拷贝,只是字面量之间的赋值,这样会丢失 Proxy 的代理链接。
ref 和 reactive 的区别
经过上面对 ref 和 reactive 的深入了解,我们可以总结下它们二者之间的基本区别:
基本区别
| reactive | ref |
|---|---|
| ❌ 只支持对象和数组(引用数据类型) | ✅ 支持基本数据类型 + 引用数据类型 |
✅ 在 <script> 和 <template> 中无差别使用 | ❌ 在 <script> 和 <template> 使用方式不同(在 <script> 中要使用 .value) |
| ❌ 重新分配一个新对象会丢失响应性 | ✅ 重新分配一个新对象不会失去响应 |
| ✅ 能直接访问属性 | ❌ 需要使用 .value 访问属性 |
| ❌ 将对象传入函数时,失去响应 | ✅ 传入函数时,不会失去响应 |
| ❌ 解构时会丢失响应性,需使用 toRefs | ✅ 解构对象时不会丢失响应性 |
在 watch 使用中的区别
使用 watch 侦听 ref 和 reactive 的方式也是不同的
- 使用 watch 侦听 ref 定义的响应式数据(参数是原始数据类型)
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
let count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`count的值变化了,新值:${newValue},旧值:${oldValue}`)
})
const changeCount = () => {
count.value += 10
}
</script>当侦听的数据是用 ref 定义的原数类型的数据时,数据发生变化的时候,就会执行 watch 函数的回调。
- 使用 watch 侦听 ref 定义的响应式数据(参数是引用数据类型的情况)
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
let count = ref({ num: 0 })
watch(count, () => {
console.log(`count的值发生变化了`)
})
const changeCount = () => {
count.value.num += 10
}
</script>
可以看到,当我们点击按钮时,界面的 count 值更新了,但控制台并没有打印 watch 相关的 输出,说明 watch 没有监听到 count 的变化。
界面 count 更新了,说明 DOM 能够更新,响应式是没问题的。但 watch 没有监听到数据变化,其实这是深度监听的问题,我们开启下深度监听之后就可以成功监听数据变化了。
还有另一种方法可以在不开启深度监听的情况下,触发 watch 的监听。
对上面的代码再进行改造下,直接侦听 count.value,但是不深度侦听:
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
let count = ref({ num: 0 })
watch(count.value, () => {
console.log(`count的值发生变化了`)
})
const changeCount = () => {
count.value.num += 10
}
</script>可以看到,DOM 更新了,控制台也打印输出了,打印一下 count.value

我们发现打印出来的 count.value 是一个 Proxy 代理对象。因为对象类型的数据经过 ref 函数加工会变成 RefImpl 包装的对象,而该对象的 value 是 Proxy 类型的。我们想成功监听 ref(参数是对象),就需要监听内部的 proxy 对象,而不是外部的 RefImpl 包装对象。
- 使用 watch 侦听 reactive 定义的响应式数据
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
let count = reactive({ num: 0 })
watch(count, () => {
console.log(`count的值发生变化了`)
})
const changeCount = () => {
count.num += 10
}
</script>
可以看到,用 watch 函数侦听 reactive 数据时,不需要添加 deep 属性,也能够对其深度侦听。
源码解读
vue3 关于 ref 和 reactive 的实现非常复杂,这里我们只看简化后的代码:
ref 源码解读
// 深响应式
export function ref(value?: unknown) {
return createRef(value, false)
}
// 浅响应式
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
function createRef(rawValue: unknown, shallow: boolean) {
// 如果传入的值已经是一个 ref,则直接返回它
if (isRef(rawValue)) {
return rawValue
}
// 否则,创建一个新的 RefImpl 实例
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
// 存储响应式的值。我们追踪和更新的就是_value。(这个是重点)
private _value: T
// 用于存储原始值,即未经任何响应式处理的值。(用于对比的,这块的内容可以不看)
private _rawValue: T
// 用于依赖跟踪的 Dep 类实例
public dep?: Dep = undefined
// 一个标记,表示这是一个 ref 实例
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 如果是浅响应式,直接使用原始值,否则转换为非响应式原始值
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果是浅响应式,直接使用原始值,否则转换为响应式值
this._value = __v_isShallow ? value : toReactive(value)
// toRaw 用于将响应式引用转换回原始值
// toReactive 函数用于将传入的值转换为响应式对象。对于基本数据类型,toReactive 直接返回原始值。
// 对于对象和数组,toReactive 内部会调用 reactive 来创建一个响应式代理。
// 因此,对于 ref 来说,基本数据类型的值会被 RefImpl 直接包装,而对象和数组
// 会被 reactive 转换为响应式代理,最后也会被 RefImpl 包装。
// 这样,无论是哪种类型的数据,ref 都可以提供响应式的 value 属性,
// 使得数据变化可以被 Vue 正确追踪和更新。
// export const toReactive = (value) => isObject(value) ? reactive(value) : value
}
get value() {
// 追踪依赖,这样当 ref 的值发生变化时,依赖这个 ref 的组件或副作用函数可以重新运行。
trackRefValue(this)
// 返回存储的响应式值
return this._value
}
set value(newVal) {
// 判断是否应该使用新值的直接形式(浅响应式或只读)
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
// 如果需要,将新值转换为非响应式原始值
newVal = useDirectValue ? newVal : toRaw(newVal)
// 如果新值与旧值不同,更新 _rawValue 和 _value
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 触发依赖更新
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}在上述代码中,ref 函数通过 new RefImpl(value) 创建了一个新的 RefImpl 实例。这个实例包含 getter 和 setter ,分别用于追踪依赖和触发更新。使用 ref 可以声明任何数据类型的响应式状态,包括对象和数组。
import { ref } from 'vue'
const state = ref({ count: 0 })
state.value.count++当我们使用 new RefImpl(value) 创建一个 RefImpl 实例时,这个实例大致上会包含以下几部分:
- 内部值:实例存储了传递给构造函数的初始值。
- 依赖收集:实例需要跟踪所有依赖于它的效果
(effect),例如计算属性或者副作用函数。这通常通过一个依赖列表或者集合来实现。 - 触发更新:当实例的值发生变化时,它需要通知所有依赖于它的效果,以便它们可以重新计算或执行。
RefImpl 类似于发布-订阅模式的设计,以下是一个简化的 RefImpl 类的伪代码实现,展示这个实现过程:
class Dep {
constructor() {
this.subscribers = new Set()
}
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
notify() {
this.subscribers.forEach((effect) => effect())
}
}
let activeEffect = null
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
class RefImpl {
constructor(value) {
this._value = value
this.dep = new Dep()
}
get value() {
// 当获取值时,进行依赖收集
this.dep.depend()
return this._value
}
set value(newValue) {
if (newValue !== this._value) {
this._value = newValue
// 值改变时,触发更新
this.dep.notify()
}
}
}
// 使用示例
const count = new RefImpl(0)
watchEffect(() => {
console.log(`The count is: ${count.value}`) // 订阅变化
})
count.value++ // 修改值,触发通知,重新执行watchEffect中的函数Dep 类负责管理一个依赖列表,并提供依赖收集和通知更新的功能。 RefImpl 类包含一个内部值 value 和一个 Dep 实例。当 value 被访问时,通过 get 方法进行依赖收集;当 value 被赋予新值时,通过 set 方法触发更新。
注意, ref 核心是返回响应式且可变的引用对象,而 reactive 核心是返回的是响应式代理,这是两者本质上的核心区别,也就导致了 ref 优于 reactive。 我们接着看下 reactive 源码实现。
reactive 源码解读
reactive 是一个函数,它接受一个对象并返回该对象的响应式代理,也就是 Proxy。
function reactive(target) {
if (target && target.__v_isReactive) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap,
)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
if (!isObject(target)) {
return target
}
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(target, baseHandlers)
proxyMap.set(target, proxy)
return proxy
}reactive 的源码相对就简单多了,它通过 new Proxy(target, baseHandlers) 创建了一个代理。这个代理会拦截对目标对象的操作,从而实现响应式。
到这里我们可以看出 ref 和 reactive 在声明数据的响应式状态上,底层原理是不一样的。ref 采用 RefImpl 对象实例, reactive 采用 Proxy 代理对象。
reactive 的局限性
在 Vue3 中,reactive API 通过 Proxy 实现了一种响应式数据的方法,尽管这种方法在性能上比 Vue2 有所提升,但 Proxy 的局限性也导致了 reactive 的局限性,这些局限性可能会影响开发者的使用体验。
reactive 的局限性主要集中在两点:
- 仅对引用数据类型有效
- 使用不当会失去响应式(参考上面关于 reactive 的缺点)
究竟是使用 ref 好 还是 reactive 好
究竟是使用 ref 好 还是 reactive 好,一直是个有争议的问题。使用 ref 时,响应性很稳定,但是到处 .vue 很是麻烦。在我们试图将一个组件从Options API迁移到成Composition API时,使用 reactive 更方便 (因为它与 data 类似),但是重构或者重新赋值时又非常容易丢失响应性。
其实单单只是 容易丢失响应性这一条,就代表 reactive 还是不太好把握的。日常的开发中我还是建议使用 ref一把梭,省的哪些乱七八糟的。毕竟尤大大也是这样推荐的。
我可能只会在一种情况下才会使用 reactive————vue 2 语言项目改到 vue3 时,为了快速的重构 data 里的数据。
