🧭 学习主线
读取响应式数据时收集依赖,修改响应式数据时触发依赖重新执行。
可以把它拆成三个关键角色:
| 角色 | 作用 | 对应代码 |
|---|---|---|
| 📦 响应式数据 | 保存值,并拦截读取和修改 | ref/ RefImpl |
| 🧲 依赖收集 | 在读取 .value时记录谁用到了这个值 | get value() |
| 🔁 触发更新 | 在修改 .value时重新执行依赖函数 | set value() |
🧠 1. 什么是响应式
Vue的响应式系统核心在于响应式对象的属性与 effect 副作用函数之间建立的依赖关系
🔹 1.1 普通函数访问响应式数据
💡 源码理解
普通函数里虽然读取了 count.value,但是 Vue 并不知道这个函数以后需要被重新执行。
原因是:这次读取没有处在 effect 的收集环境里,所以 count 没有机会把这个函数记录下来。后面就算 count.value 变了,也找不到要重新执行的函数。
import { ref } from 'vue'
const count = ref(0)
// 普通函数
function fn(){
console.log(count.value)
}
fn() // 打印 0
setTimeout(()=>{
count.value = 1 // 修改值不会触发 fn 重新执行
})
虽然fn读取了响应式数据count.value,但由于它不是在effect中执行的,因此当count.value发生变化时,该函数不会重新执行
✅ 这里要记住
ref 本身只是让数据具备“可追踪”的能力,但是否真的追踪,还要看读取发生在哪里。
🔹 1.2 effect中访问响应式数据
💡 源码理解
effect 的作用可以先简单理解成:告诉响应式系统“这个函数是需要被追踪的”。
当 effect 内部读取 count.value 时,count 就可以把当前正在执行的函数保存起来。等后面 count.value 被修改时,再把这个函数拿出来重新执行。
import { ref, effect } from 'vue'
const count = ref(0)
effect(()=>{
console.log(count.value) // 首次执行打印 0
})
setTimeout(()=>{
count.value = 1 // 触发 effect 重新执行,打印 1
})
🔁 执行流程
effect先执行一次传入的函数- 函数执行时读取
count.value get value()被触发,开始收集依赖setTimeout中修改count.valueset value()被触发,通知依赖重新执行
🛠️ 2、源码中去实现
📁 1、在reactivity/src中新建三个ts文件
📄 1、新建effect.ts
💡 源码理解
第一版 effect 先不要想复杂,它最基础的能力就是:接收一个函数,并立即执行它。
这一步只是搭建入口,后面才会继续给它加“当前正在执行的 effect”这个状态。
export function effect(fn){
fn()
}
📄 2、新建index.ts
💡 源码理解
index.ts 的作用是统一出口。
以后外部使用时,不需要分别去找 ref.ts 和 effect.ts,只要从当前模块入口导入即可。
export * from './ref'
export * from './effect'
📄 3、新建ref.ts
💡 源码理解
ref(value) 的本质不是直接返回原始值,而是把原始值包一层对象。
这样做的原因是:只有包成对象之后,才可以通过 get value() 和 set value() 拦截读取与修改。
class RefImpl{
constructor(value) {
this._value = value
}
}
export function ref(value){
return new RefImpl(value)
}
✅ 这里要记住
count 不是数字 0,而是一个 RefImpl 实例;真正的值被放在 _value 里。
📁 2、在reactivity/src同级新建examples文件夹(存放测试案例)
📄 1、新建01-demo.html
✅ 1、1 vue中实现1s之后打印1
💡 源码理解
这个案例先用 Vue 官方的 ref 和 effect 跑通效果,目的是给后面自己实现源码一个对照目标。
学习源码时不要一上来就写实现,先确认最终行为是什么:首次打印一次,1 秒后值变化,再打印一次。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Document</title>
</head>
<body>
<script type="module">
import { ref, effect } from '../../../node_modules/vue/dist/vue.esm-browser.prod.js'
const count = ref(0)
effect(() => {
console.log('effect1 count.value =>', count.value)
})
setTimeout(() => {
count.value = 1
}, 1000)
</script>
</body>
</html>
✅ 1、2 我们如何去实现?
逐步去完善reactivity/src的ts文件
📄 1、ref.ts
💡 源码理解
这一版 ref.ts 开始具备响应式的核心雏形:
| 代码位置 | 作用 |
|---|---|
_value | 保存真实的值 |
[ReactiveFlgs.IS_REF] = true | 给当前对象打上 ref 标记 |
get value() | 读取 .value时触发 |
set value() | 修改 .value时触发 |
isRef | 判断一个值是不是 ref |
这里的 console.log(' 有人访问我了 ') 和 console.log(' 我的值变了 ') 只是为了观察流程。真正源码里,这两个位置分别会做“依赖收集”和“触发更新”。
enum ReactiveFlgs = {
IS_REF = '__v_isRef' // ref 标记,证明是一个 ref
}
/*
* Ref 的类
* */
class RefImpl{
// 保存实际的值
_value;
// ref 标记,证明是一个 ref
[ReactiveFlgs.IS_REF] = true
constructor(value) {
this._value = value
}
get value(){
// 收集依赖
console.log(' 有人访问我了 ')
return this._value;
}
set value(newValue){
// 触发更新
console.log(' 我的值变了 ')
this._value = newValue
}
}
/*
* 判断是不是一个 ref
* @params value
* */
export function isRef(value){
return !!(value && value[ReactiveFlgs.IS_REF])
}
export function ref(value){
return new RefImpl(value)
}
✅ 这里要记住
响应式不是值自己会动,而是读取和修改这两个动作被拦截了。
❓ 如何去收集依赖?如何去触发更新???
🧩 源码思路拆解
要让 count.value = 1 后重新执行 effect,需要解决两个问题:
| 问题 | 解决方式 |
|---|---|
| 读取时怎么知道是谁在读? | 用一个全局变量保存当前正在执行的 effect |
| 修改时怎么知道通知谁? | 在 ref 实例上保存之前收集到的 effect |
所以整体思路是:
effect(fn)执行前,把fn标记成当前活跃的副作用函数- 执行
fn fn内部读取count.valueget value()发现当前有活跃的 effect,就把它保存到subs- 修改
count.value set value()执行subs
📄 effect.ts
💡 源码理解
activeSub 可以理解成一个临时变量,用来保存“当前正在被收集的函数”。
为什么需要这个变量?因为 get value() 触发的时候,它本身并不知道是谁读取了 .value。所以需要 effect 在外面先把当前函数放到一个公共位置,get value() 再从这个位置取到它。
// 用来保存当前正在执行的 effect
// 相当于示例中的
// () => {
// console.log('count.value =>', count.value)
// }
export let activeSub
export function effect(fn){
activeSub = fn()
activeSub() // 就是执行 fn()
activeSub = underfined
}
✅ 这里要记住
学习这一段时重点看思想:effect 负责打开收集窗口,ref.value 的 getter 负责在窗口打开时把依赖记下来。
📄 ref.ts
💡 源码理解
最终这版 ref.ts 把依赖收集和触发更新串起来了。
关键点在这两处:
| 位置 | 做的事情 |
|---|---|
get value() | 如果存在 activeSub,说明当前读取发生在 effect中,于是保存依赖 |
set value() | 修改值之后,执行之前保存的依赖函数 |
import { activeSub } from './effect'
enum ReactiveFlgs = {
IS_REF = '__v_isRef' // ref 标记,证明是一个 ref
}
/*
* Ref 的类
* */
class RefImpl{
// 保存实际的值
_value;
// ref 标记,证明是一个 ref
[ReactiveFlgs.IS_REF] = true
// 保存和 effect 之间的关联关系
subs
constructor(value) {
this._value = value
}
get value(){
// 收集依赖
if(activeSub){
// 如果 activeSub 有,保存起来,等我更新时触发
this.subs = activeSub
}
return this._value;
}
set value(newValue){
// 触发更新
this._value = newValue
this.subs?.() // 可选链 ?. activeSub 赋值给 this.subs 可能是空的
}
}
/*
* 判断是不是一个 ref
* @params value
* */
export function isRef(value){
return !!(value && value[ReactiveFlgs.IS_REF])
}
export function ref(value){
return new RefImpl(value)
}
🧠 最后总结
这一节可以先不用追求一次性还原 Vue 完整源码,先把最小响应式模型跑通:
| 阶段 | 发生了什么 |
|---|---|
| 创建 | ref(0)创建一个 RefImpl实例 |
| 首次执行 | effect执行传入的函数 |
| 读取 | 访问 count.value,触发 get value() |
| 收集 | get value()把当前 effect 保存起来 |
| 修改 | count.value = 1,触发 set value() |
| 更新 | set value()重新执行之前保存的 effect |
源码学习时最重要的是抓住这个闭环:
effect 执行函数 → 函数读取响应式数据 → 响应式数据收集函数 → 数据变化 → 函数重新执行