上一章:# 手把手搭建Vue轮子从0到1:4. Reactivity 模块的实现
先思考下:
- ref 函数是如何进行实现的?
- ref 可以构建简单数据类型的响应性吗?
- 为什么 ref 类型的数据,必须要通过 .value 访问值呢?
这节我们就需要解决以上三个问题。
5.1. 阅读源码:ref 复杂数据类型的响应性
- 创建测试实例
创建 packages\vue\examples\my-examples\ref.html 文件
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ref的响应性</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script>
const { effect, ref } = Vue;
const obj = ref({
count: 0
});
effect(() => {
document.querySelector('#app').innerHTML = `count is: ${obj.value.count}`;
});
setTimeout(() => {
obj.value.count++;
}, 1000);
</script>
</html>
在 packages\reactivity\src\ref.ts 中打断点
5.1.1. ref 函数
- 可以看到,在ref 函数中,直接触发了 createRef 函数

- 在 createRef 中,进行了判断:如果当前已经是一个 ref 类型数据则直接返回,否则返回 RefImpl 类型的实例。

-
那么 RefImpl 是什么?
a. RefImpl 是同样位于 packages\reactivity\src\ref.ts 之下的一个类
b. 该类的构造函数中,执行了一个 toReactive 的方法,传入了 value 并把返回值赋值给了 this._value
toReactive的作用:将数据分成了两种类型
- 复杂数据类型:调用了 reactive 函数,即把 value 变为响应性的。
- 简单数据类型:直接把 value 原样返回
c. 提供了一个分别被 get 和 set 标记的函数 value
- 当执行 xxx.value 时,会触发 get 标记
- 当执行 xxx.value = xxx 时,会触发 set 标记

- ref 函数执行完成
总结:
- 对于 ref 而言,主要生成了 RefImpl 的实例
- 在构造函数中对传入的数据进行了处理:
- 复杂数据类型:转为响应性的 proxy 实例
- 简单数据类型:不去处理
- RefImpl 分别提供了 get value、set value 以此来完成对 getter 和 setter 的监听,需要注意这里并没有使用 proxy
5.1.2. effect函数
当 ref 函数执行完成之后,测试实例开始执行 effect 函数。
effect 函数我们之前跟踪过它的执行流程,我们知道整个 effect 主要做了3件事情:
- 生成 ReactiveEffect 实例
- 触发fn方法,从而激活 getter
- 建立了 targetMap 和 activeEffect 之间的联系
- dep.add(activeEffect)
- activeEffect.deps.push(dep)
通过以上可知,effect 中会触发 fn 函数,也就是说会执行 obj.value.name,那么根据 get value 机制,此时会触发 Refimpl 的 get value 方法。
5.1.3. get value()
- 在 get value 中会触发 trackRefValue 方法
- 触发 trackEffects 函数,并且在此时为 ref 新增了一个 dep 属性:
js
trackEffects(ref.dep || (ref.dep = createDep())...)
- trackEffects 的主要作用就是:收集所有的依赖
- get value 执行完成

总结:整个 get value 的处理逻辑还是比较简单的,主要还是通过之前的 trackEffects 属性来收集依赖。
5.1.4. 再次触发 get value()
最后就是在1秒后,修改数据源
js
obj.value.count++;
这里需要思考一个很关键的问题:此时会触发 get value 还是 set value?
以上代码可以解析成:
js
const value = obj.value
value.count = value.count + 1
通过以上代码,可以清晰的知道触发的应该是 get value 函数。
在 get value 函数中:
- 再次执行 trackRefValue 函数:
但此时的 activeEffect 为 undefined,所以不会执行后续逻辑
- 返回 this._value:
通过 构造函数,我们可知此时的 this._value 是经过 toReactive 函数过滤之后的数据,在当前实例中为 proxy 实例。
- get value 执行完成
总结:
- const value 是 proxy 类型的实例,即:代理对象,被代理对象为 {count: 0}
- 执行 value.count = value.count + 1,本质上是出发了 proxy 的 setter
- 根据 reactive 的执行逻辑可知,此时会触发 trigger 触发依赖
- 最后,修改视图
5.1.5. 总结:
- 对于 ref 函数,会返回 RefImpl 类型的实例
- 在该实例中,会根据传入的数据进行分开处理
- 复杂数据类型:转化为 reactive 返回的 proxy 实例
- 简单数据类型:不做处理
- 无论执行 obj.value.count 还是 obj.value.count++ 本质上都是触发了 get value
- 之所以会进行响应性是因为 obj.value 是一个 reactive 函数生成的 proxy
5.2. 构建 ref 函数
tsconfig.json
json
// 入口
"include": ["packages/*/src", "packages/shared"]
packages\shared\index.ts
ts
/**
* 判断两个值是否相等(对比两个数据是否发生了变化)
* @param value
* @param oldValue
* @returns
*/
export function hasChanged(value: any, oldValue: any): boolean {
return !Object.is(value, oldValue)
}
packages\reactivity\src\ref.ts
ts
import { Dep, createDep } from "./dep"
import { triggerEffects, trackEffects } from "./effect"
import { hasChanged } from "../../shared"
import { reactive } from "./reactive"
/**
* 响应式转换
* 将值转换为响应式对象
* @param value
* @returns
*/
function toReactive(value: any) {
return typeof value === 'object' && value !== null
? reactive(value) // 对象转为响应式
: value // 基础类型直接返回
}
class RefImpl<T> {
// 响应式数据(当前值)
private _value: T
// 原始数据
private _rawValue: T
// 依赖集合
public readonly _dep: Dep | null = null
constructor(value: T, public readonly _shallow: boolean = false) {
// 原始数据
this._rawValue = value
// 初始化 _value,转换为响应式
this._value = toReactive(value)
// 初始化依赖
this._dep = createDep()
}
/**
* 收集依赖
* 当 ref 的 value 属性被访问时,收集依赖
* @returns
*/
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
/**
* 触发更新
* 当 ref 的 value 属性被修改时,触发依赖
* @param newVal
*/
set value(newVal) {
// newVal 为新数据
// this._rawValue 为原始数据
// 如果新数据和原始数据不一致,则更新数据
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = toReactive(newVal)
triggerRefValue(this)
}
}
}
/**
* 依赖收集
* 为 ref 的 value 属性收集依赖
* @param ref
*/
export function trackRefValue(ref: RefImpl<any>) {
if (ref._dep) {
// 收集当前 effect 到依赖种
trackEffects(ref._dep)
}
}
/**
* 依赖触发
* 为 ref 的 value 属性触发依赖
* @param ref
*/
export function triggerRefValue(ref: RefImpl<any>) {
if (ref._dep) {
// 触发所有依赖的 effect
triggerEffects(ref._dep)
}
}
/**
* 创建 ref 对象
* @param value
* @returns
*/
export function ref<T>(value: T) {
return new RefImpl(value)
}
packages\reactivity\src\index.ts
ts
export { reactive } from './reactive'
export { effect } from './effect'
export { ref } from './ref'
packages\vue\src\index.ts
ts
export { reactive, effect, ref } from '@vue/reactivity'
创建测试实例,运行。测试成功,表示代码完成。
packages\vue\examples\ref-mvp.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ref MVP 实现演示</title>
<script src="../dist/vue.js"></script>
</head>
<body>
<div id="app">
<h2>Ref MVP 实现演示</h2>
<div id="content"></div>
<button onclick="changeValue()">改变值</button>
<button onclick="changeObject()">改变对象</button>
</div>
</body>
<script>
const { effect, ref } = Vue
// 基础类型 ref
const count = ref(0)
const message = ref('Hello Vue Mini!')
// 对象类型 ref
const user = ref({
name: '张三',
age: 25
})
// 响应式效果
effect(() => {
const content = document.getElementById('content')
content.innerHTML = `
<p>基础类型 ref:</p>
<p>count: ${count.value}</p>
<p>message: ${message.value}</p>
<p>对象类型 ref:</p>
<p>user.name: ${user.value.name}</p>
<p>user.age: ${user.value.age}</p>
`
})
// 改变基础类型值
function changeValue() {
count.value++
message.value = '值已更新: ' + Date.now()
}
// 改变对象值
function changeObject() {
user.value.name = '李四'
user.value.age = 30
}
// 3秒后自动更新
setTimeout(() => {
count.value = 100
message.value = '自动更新完成!'
}, 3000)
</script>
</html>
核心特性:
- 基础类型响应式:通过 getter/setter 实现
- 对象类型响应式:内部使用 reactive() 转换
- 依赖收集:读取 .value 时收集当前 effect
- 依赖触发:设置 .value 时触发所有依赖
- 值变化检测:使用 hasChanged() 避免不必要的更新
ref 简单数据类型响应性:
简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。只是因为 vue 通过 set value() 的语法,把 函数调用变成了属性调用的形式,让我们能主动调用该函数,来完成一个"类似于"响应性的结构。
5.3. 总结
- ref 函数是如何进行实现的?
ref 函数本质是生成了一个 RefImpl 类型的实例对象,通过 get 和 set 标记处理了 value 函数
- ref 可以构建简单数据类型的响应性吗?
ref 可以构建简单数据类型的响应性
- 为什么 ref 类型的数据,必须要通过 .value 访问值呢?
- 因为 ref 需要处理简单数据类型的响应性,但是对于简单数据类型而言,它无法通过 proxy 建立代理
- 所以 vue 通过 get value() 和 set value() 定义了两个属性函数,通过 主动 触发这两个函数(属性调用)的形式来进行 依赖收集 和 触发依赖
- 所以必须通过 .value 来保证响应性