Vue3 - 使用 ref() 和 reactive() 实现响应式数据

三葉Leaves Author
什么是响应式数据

JS 里的变量值发生变化的时候,并不能总是更新到页面上,原生 JS 为了实现这个目标往往要写大量 element.innerHtml = xxx 语句。

Vue3 的响应式数据可以实现当 JS 变量值发生变化时候,自动渲染到页面上。

响应式系统的根基:reactive

个人认为学习 Vue3 响应式的正确的学习顺序应该是从它学起。

1. 处理什么,怎么处理?

  • 处理的是引用数据类型的数据,比如对象或者数组

reactive 像是一位智能管家,你把 JS 里普通的数据交给它托管以后,数据就能具有响应式能力。

reactive() 包裹,可将普通对象数据变为响应式的数据:

1
2
const myData = { count: 0, name: '办公室' };  
const state = reactive(myData); // state 就是被智能管家代理后的 myData

更厉害的是,其具有深度响应能力,也就是对象内部无论嵌套多少层仍具有响应能力。

2. 返回什么,返回值怎么用?

返回什么

对上面的例子,我们可以 log 一下其返回值:

1
console.log(state)

结果如下:

可以看到 reactive(obj) 的返回值是一个 ES6 Proxy 对象

那到底什么是 Proxy 对象?

关于 ES6 Proxy

你可以把 Proxy 想象成给你的普通对象雇佣了一个“智能管家”。你不再直接接触这个对象,而是所有操作都通过这个管家来完成。

这个“管家”(Proxy) 会在你对 state 做两件事时进行拦截:

  1. 读取属性时(get 拦截) -> 执行依赖收集 (Tracker)

当你试图读取 state.count 时(比如在模板 <p>{{ state.count }}</p> 中),管家会拦截这个操作。他会:

  • 先把 count 的值 0 交给你。
  • 然后在一个小本本上记下:“哦,‘视图渲染’这个家伙正在关心 state.count 的值。” 这就是依赖收集
  1. 修改属性时(set 拦截) -> 执行触发更新 (Trigger)

当你执行 state.count++ 时,管家同样会拦截这个操作。他会:

  • 先更新原始对象 myDatacount 值为 1
  • 然后,他会翻开他的小本本,找到所有关心 state.count 的家伙(刚才记下的“视图渲染”),并对他们大喊:“count 变了,快更新!” 这就是触发更新

所以,reactive 的本质就是利用 Proxy 在你访问修改对象属性的瞬间“做手脚”,悄无声息地完成了依赖收集和触发更新。

返回的东西怎么用?

你可以像使用普通对象一样取值,比如 x = state.count

关于重新赋值

唯一要注意的是,如果你需要重新赋值,而且是赋值一个新对象,事情就变得复杂了。下面两种做法都不可取:

定义:

1
2
3
4
let person = reactive({  
name: 'leaves',
age: 18
})

错误示例:这两种方式都会导致 person 失去响应能力

  • 错误示例1:

你试图直接赋值:

1
2
3
4
person = {  
name: 'crain',
age: 22
}

其实认真读前文你就知道,person 早就不是一个普通对象了,它是一个 Proxy 对象的实例,你把它赋值成普通对象,它当然会失去响应性。

  • 错误示例2:

你试图新建一个新的代理:

1
2
3
4
person = reactive({  
name: 'crain',
age: 22
})

这个往往会导致很多新手困惑。

我们需要知道,当 vue 第一次渲染你的模板语法,亦或者是 watch 的时候:

1
2
<p @click="changePerson">姓名:{{ person.name }}</p>  
<p @click="changeAge">年龄:{{ person.age }}</p>

这相当于一次 get 行为,其会触发第一个代理的依赖收集(Tracker)那边的工作,并且建立好和第一个代理之间的依赖关系

你再次创建了一个新的代理,但是之前的模板语法里的变量并不会触发新的 get ,所以新的代理那边并没有建立任何依赖关系,视图仍然在“监听”那个被你丢弃的旧代理,响应式链接断开了。所以新代理自然会失去响应能力了。

事实上,新的代理对象本身是响应式的,如果你有其他代码(比如一个新的 watch)去监听这个新代理,它会正常工作。只是通常情况下这样写,往往导致的都是超出预期的行为。

正确做法

你大可以直接修改属性的值,如果非要传一整个对象,

使用浅拷贝即可解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
function changePerson() {
// 不要重新赋值,而是修改属性
person.name = 'crain';
person.age = 22;

// 或者,如果你有一个新对象,可以用 Object.assign
const newData = { name: 'crain', age: 22 };
Object.assign(person, newData);
// Object.assign 会遍历 newData 的属性并将其逐个赋值给 person,
// 这会触发 person 代理的 set 拦截器,从而实现响应式更新。
}

因为浅拷贝只是传传值,并不会修改其地址引用。

精巧的包装盒:ref()

处理什么,怎么处理?

正确认识 reactive 后,理解 ref() 就变得轻而易举了。

reactive 虽好,但有一个局限:

  • 只能处理引用数据类型的数据,比如对象或者数组。对基本数据类型无能为力。

ref() 则是两者都能处理。

  • 如果往其括号里传的是 引用数据类型的数据,其会自动递归的交给 reactive 处理,这样数据自然会变成响应式的;

  • 如果往里面传的是基本类型的数据,ref() 会把这个数据包装进内部的 .value 属性里存着。

既然我不能直接让一个数字变得智能,那我可以用一个“盒子”(对象)把它包起来,然后让这个“盒子”变得智能。

而这个”盒子“的内部原理和 Proxy 很像,也有 get 和 set 那一套,所以也是响应性的。

2. 返回什么,返回值怎么用?

返回什么

ref(value) 的返回值是一个 RefImpl 类的实例对象。这是一个 Vue 内部定义的类,我们可以把它理解成一个结构化的小盒子。

返回的这个对象核心就是其的 value 属性,这个属性并不是一个简单的值,而是通过 JavaScript 的 Object.defineProperty 或 class 的 get/set 语法定义的访问器属性 (Accessor Property)

底层原理:带有 gettersetter 的类实例。

其源码可能像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class RefImpl {
private _value; // 私有属性,存储真实的值
public readonly __v_isRef = true; // 一个标记,让 Vue 知道这是一个 ref

constructor(value) {
// 关键点:如果传入的是对象,则内部自动用 reactive 包裹
this._value = isObject(value) ? reactive(value) : value;
}

// value 的 getter
get value() {
// 依赖收集:当读取 .value 时,告诉 Vue 当前的副作用依赖我
track(this, 'value');
return this._value;
}

// value 的 setter
set value(newValue) {
// 检查新旧值是否相同,避免不必要的更新
const hasChanged = !Object.is(newValue, this._value_raw); // _value_raw 是未被代理的原始值
if (hasChanged) {
// 如果新值是对象,也用 reactive 包裹
this._value = isObject(newValue) ? reactive(newValue) : newValue;
// 触发更新:通知所有依赖我的副作用重新运行
trigger(this, 'value');
}
}
}

返回的东西怎么用?

一句话总结: 加上 .value

因为不管是基本数据类型还是引用,都存在这个 .value 里。

示例代码:

1
2
3
4
let person = ref({  
name: 'leaves',
age: 18
})

访问:

1
console.log(person.value.age)

如果你给你的 vscode (或者 cursor 这种基于 vscode 生态的 IDE 装上 Vue Official 官方插件,并且在设置中开启:


Vue › Auto Insert: Dot Value

  • [x] Auto-complete Ref value with .value.

编辑器还会自动帮你 .value,不过 webstorm 暂时没发现这功能。

两者之间的桥梁:toRefs()

干嘛用的?

当你想对一个 reactive 定义出的东西解构赋值:

1
2
3
const state = reactive({ count: 1, name: 'Vue' });

const {count, name} = state

你会发现,countname 都将失去响应能力,对其的修改不会触发视图更新。

这是因为 { ...state } 实际上是 { count: state.count, name: state.name }。它把 state 对象里的值取出来,赋给了普通的 JS 变量,普通的变量当然没有响应能力了!

toRefs() 就是为了解决这个问题而生的。

做什么用? toRefs() 可以将一个响应式对象(由 reactive 创建)的所有属性都转换成一个个独立的 ref

  • 往里面传什么?

你应当往 toRefs(reactiveObject) 的括号里装一个 reactive 创建的响应式对象。

  • 得到什么?

返回值是一个新的、普通的 JavaScript 对象

虽然这个对象普通,但是对象里装的东西可一点也不普通。
原来对象里装的可都是死的 JS 变量值,现在里面装的都是 ObjectRefImpl 类的实例,这个东西很像是上文中 ref() 创建出的 RefImpl 类的实例,但是更加特殊(区别在于其内部不存值,下文中会说)

我们改写上面例子里的代码,这次用 toRefs(),如此一来就可以安全地解构啦。

1
const {count, name} = toRefs(state)

底层原理:属性的代理人

当你调用 const stateAsRefs = toRefs(state); 时,toRefs 会遍历 state 对象的所有属性(比如 countname),然后为每一个属性创建一个特殊的 ref,也就是 ObjectRefImpl

这个特殊的 ref(比如 stateAsRefs.count)有什么特别之处呢?

  • 读取 stateAsRefs.count.value 时,它内部实际上是在访问原始 state 对象的 count 属性 (state.count)。
  • 修改 stateAsRefs.count.value = 2 时,它内部实际上是在修改原始 state 对象的 count 属性 (state.count = 2)。

所以,toRefs 创建的 ref 就像是原始 state 对象各个属性的“授权代理人”,

它本身不存值

它的所有操作最终都会导向并作用于那个原始的、被 Proxy 代理的 state 对象。

往更深了说,可以看看这段伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 简化版的 ObjectRefImpl 伪代码
class ObjectRefImpl {
constructor(sourceObject, key) {
this._object = sourceObject; // 持有原始 reactive 对象 (e.g., state)
this._key = key; // 持有属性名 (e.g., 'count')
}

get value() {
// 它不需要自己 track(),因为当它访问 this._object[this._key] 时,
// 那个 reactive 对象的 Proxy get 陷阱已经被触发,完成了依赖收集。
return this._object[this._key];
}

set value(newValue) {
// 它不需要自己 trigger(),因为当它修改 this._object[this._key] 时,
// 那个 reactive 对象的 Proxy set 陷阱已经被触发,完成了派发更新。
this._object[this._key] = newValue;
}
}

// const { count } = toRefs(state)
// 里面的 count 就好比是 new ObjectRefImpl(state, 'count')

其也有 get 和 set,但是操作都指向原始对象,修改和读取的都是原始对象的值,自己是不存值的。

  • 标题: Vue3 - 使用 ref() 和 reactive() 实现响应式数据
  • 作者: 三葉Leaves
  • 创建于 : 2025-06-21 00:00:00
  • 更新于 : 2025-07-10 13:40:50
  • 链接: https://blog.oksanye.com/c70bff904dce/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论