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

JS 里的变量值发生变化的时候,并不能总是更新到页面上,原生 JS 为了实现这个目标往往要写大量 element.innerHtml = xxx
语句。
Vue3 的响应式数据可以实现当 JS 变量值发生变化时候,自动渲染到页面上。
响应式系统的根基:reactive
个人认为学习 Vue3 响应式的正确的学习顺序应该是从它学起。
1. 处理什么,怎么处理?
- 处理的是引用数据类型的数据,比如对象或者数组
reactive 像是一位智能管家,你把 JS 里普通的数据交给它托管以后,数据就能具有响应式能力。
用 reactive()
包裹,可将普通对象数据变为响应式的数据:
1 | const myData = { count: 0, name: '办公室' }; |
更厉害的是,其具有深度响应能力,也就是对象内部无论嵌套多少层仍具有响应能力。
2. 返回什么,返回值怎么用?
返回什么
对上面的例子,我们可以 log 一下其返回值:
1 | console.log(state) |
结果如下:
可以看到 reactive(obj)
的返回值是一个 ES6 Proxy
对象。
那到底什么是 Proxy
对象?
关于 ES6 Proxy
你可以把 Proxy
想象成给你的普通对象雇佣了一个“智能管家”。你不再直接接触这个对象,而是所有操作都通过这个管家来完成。
这个“管家”(Proxy
) 会在你对 state
做两件事时进行拦截:
- 读取属性时(get 拦截) -> 执行依赖收集 (Tracker)
当你试图读取 state.count
时(比如在模板 <p>{{ state.count }}</p>
中),管家会拦截这个操作。他会:
- 先把
count
的值0
交给你。 - 然后在一个小本本上记下:“哦,‘视图渲染’这个家伙正在关心
state.count
的值。” 这就是依赖收集。
- 修改属性时(set 拦截) -> 执行触发更新 (Trigger)
当你执行 state.count++
时,管家同样会拦截这个操作。他会:
- 先更新原始对象
myData
的count
值为1
。 - 然后,他会翻开他的小本本,找到所有关心
state.count
的家伙(刚才记下的“视图渲染”),并对他们大喊:“count
变了,快更新!” 这就是触发更新。
所以,reactive
的本质就是利用 Proxy
在你访问和修改对象属性的瞬间“做手脚”,悄无声息地完成了依赖收集和触发更新。
返回的东西怎么用?
你可以像使用普通对象一样取值,比如 x = state.count
。
关于重新赋值
唯一要注意的是,如果你需要重新赋值,而且是赋值一个新对象,事情就变得复杂了。下面两种做法都不可取:
定义:
1 | let person = reactive({ |
错误示例:这两种方式都会导致 person 失去响应能力
- 错误示例1:
你试图直接赋值:
1 | person = { |
其实认真读前文你就知道,person
早就不是一个普通对象了,它是一个 Proxy
对象的实例,你把它赋值成普通对象,它当然会失去响应性。
- 错误示例2:
你试图新建一个新的代理:
1 | person = reactive({ |
这个往往会导致很多新手困惑。
我们需要知道,当 vue 第一次渲染你的模板语法,亦或者是 watch
的时候:
1 | <p @click="changePerson">姓名:{{ person.name }}</p> |
这相当于一次 get
行为,其会触发第一个代理的依赖收集(Tracker)那边的工作,并且建立好和第一个代理之间的依赖关系。
你再次创建了一个新的代理,但是之前的模板语法里的变量并不会触发新的 get
,所以新的代理那边并没有建立任何依赖关系,视图仍然在“监听”那个被你丢弃的旧代理,响应式链接断开了。所以新代理自然会失去响应能力了。
事实上,新的代理对象本身是响应式的,如果你有其他代码(比如一个新的 watch)去监听这个新代理,它会正常工作。只是通常情况下这样写,往往导致的都是超出预期的行为。
正确做法
你大可以直接修改属性的值,如果非要传一整个对象,
使用浅拷贝即可解决这个问题:
1 | function changePerson() { |
因为浅拷贝只是传传值,并不会修改其地址引用。
精巧的包装盒: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)。
底层原理:带有 getter
和 setter
的类实例。
其源码可能像这样:
1 | class RefImpl { |
返回的东西怎么用?
一句话总结: 加上 .value
因为不管是基本数据类型还是引用,都存在这个 .value 里。
示例代码:
1 | let person = ref({ |
访问:
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 | const state = reactive({ count: 1, name: 'Vue' }); |
你会发现,count
和 name
都将失去响应能力,对其的修改不会触发视图更新。
这是因为 { ...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
对象的所有属性(比如 count
和 name
),然后为每一个属性创建一个特殊的 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 | // 简化版的 ObjectRefImpl 伪代码 |
其也有 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 进行许可。