大家好,我是作曲家种太阳,本次的专栏会带你一步步实现一个mini-vue3,每个小节都都回有一些测试,验证当前的一个逻辑,并且我已经把代码上传到github上了,可以根据每个章节去看对应的源码提交记录。
本章介绍循序渐进的介绍vue3的响应式系统的 ref 的实现
ref 作用
在 Vue3 的响应式系统中,ref 是用来为基本数据类型(如字符串、数字、布尔值等)创建响应式引用的 API,同时也用于保持复杂对象结构的响应性不丢失,尤其在解构和模板语法中极其重要。
ref实现
/packages/reactivity/src/ref.ts
import { hasChanged } from '@vue/shared'
import { createDep, Dep } from './dep'
import { activeEffect, trackEffects, triggerEffects } from './effect'
import { toReactive } from './reactive'
export interface Ref<T = any> {
value: T
}
/**
* ref 函数
* @param value unknown
*/
export function ref(value?: unknown) {
return createRef(value, false)
}
/**
* 创建 RefImpl 实例
* @param rawValue 原始数据
* @param shallow boolean 形数据,表示《浅层的响应性(即:只有 .value 是响应性的)》
* @returns
*/
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
public dep?: Dep = undefined
// 是否为 ref 类型数据的标记
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 如果 __v_isShallow 为 true,则 value 不会被转化为 reactive 数据,即如果当前 value 为复杂数据类型,则会失去响应性。对应官方文档 shallowRef :https://cn.vuejs.org/api/reactivity-advanced.html#shallowref
this._value = __v_isShallow ? value : toReactive(value)
// 原始数据
this._rawValue = value
}
/**
* get 语法将对象属性绑定到查询该属性时将被调用的函数。
* 即:xxx.value 时触发该函数
*/
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
set value(newVal) {
/**
* newVal 为新数据
* this._rawValue 为旧数据(原始数据)
* 对比两个数据是否发生了变化
*/
if (hasChanged(newVal, this._rawValue)) {
// 更新原始数据
this._rawValue = newVal
// 更新 .value 的值
this._value = toReactive(newVal)
// 触发依赖
triggerRefValue(this)
}
}
}
/**
* 为 ref 的 value 进行依赖收集工作
*/
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
/**
* 为 ref 的 value 进行触发依赖工作
*/
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
/**
* 指定数据是否为 RefImpl 类型
*/
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
ref.html 验证ref逻辑
/packages/reactivity/src/ref.html
<!DOCTYPE html>
<html lang="en">
<body>
<div id="app"></div>
</body>
<head>
<meta charset="UTF-8">
<script src="../../dist/vue.js"></script>
</head>
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
// 调用 effect 方法
effect(() => {
console.log("effect-----------")
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
console.log(obj.value.name)
}, 2000);
</script>
</html>
回顾:关于 ref 的三个关键问题
❓问题一:ref 函数是如何实现的?
ref 函数的本质,是创建了一个 RefImpl 类的实例对象。这个类中通过 getter 和 setter 对 .value 属性进行了响应式封装。
当访问 .value 时,会触发 get,进而收集依赖(track)。
当修改 .value 时,会触发 set,进而触发依赖(trigger)。
❓问题二:ref 可以构建简单数据类型的响应性吗?
可以。这是 ref 存在的意义之一!
由于 Proxy 无法代理基本类型(如 string、number、boolean),所以 Vue3 提供了 ref 来解决这个问题,让这些类型也具备响应式能力。
❓问题三:为什么 ref 类型的数据必须通过 .value 来访问?
✅ 回答:
这是为了兼容基本类型响应式而设计的机制。
基本类型不能被 Proxy 代理
所以 Vue 用 RefImpl 包了一层,定义了 .value 的 getter 和 setter
你每次访问 .value,其实是调用 get value(),这样 Vue 才能执行依赖收集或触发更新
因此,为了实现这套响应机制,我们必须通过 .value 来访问 ref 中的值。
小结
这一张我们学习了ref的实现,可以看出ref和reactive的实现完全不一样
| 对比项 | ref | reactive |
|---|---|---|
| 主要作用 | 为任意值(包括基本类型)创建响应式引用 | 将对象或数组整体转为响应式 |
| 支持的数据类型 | ✅ 基本类型 + 对象 | ❌ 仅支持对象、数组、Map、Set |
| 访问方式 | 通过 .value 访问和修改值 | 直接访问对象属性 |
| 内部实现机制 | 借助 RefImpl + getter/setter | 借助 Proxy 实现属性劫持 |
| 响应式深度 | 默认深响应(对象会被递归转为 reactive) | 默认深响应,嵌套对象属性也响应式 |
| 依赖收集触发点 | 访问 .value 时触发 track() | 访问属性时触发 track() |
| 解构后响应性 | ❌ 会丢失响应性(可用 toRef / toRefs 保持响应) | ❌ 同样解构会丢失响应性,需配合 toRefs |
| 使用场景 | - 基本类型响应式 - 模板绑定 - DOM 引用 - 解构安全 | - 响应式对象状态 - 表单数据 - 嵌套结构 |
| 示例 | const count = ref(0)count.value++ | const state = reactive({ name: '张三' })state.name = '李四' |
| 与组合式 API 兼容性 | 非常适合 <script setup>,单值逻辑处理更清晰 | 常与 ref 配合使用,适合管理复杂结构状态 |
💡 小结:基本类型用
ref,对象结构用reactive,复杂情况可二者结合使用。