第五章节 响应式的ref实现【手摸手带你实现一个vue3】

89 阅读4分钟

大家好,我是作曲家种太阳,本次的专栏会带你一步步实现一个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的实现完全不一样

对比项refreactive
主要作用为任意值(包括基本类型)创建响应式引用将对象或数组整体转为响应式
支持的数据类型✅ 基本类型 + 对象❌ 仅支持对象、数组、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,复杂情况可二者结合使用。