
前言
JavaScript
const count = ref(0)
effect(() => {
console.log('count.value ==>', count.value);
})
setTimeout(() => {
count.value++
}, 1000)
昨天我们的目标是让一段简单的 ref
和 effect
代码能够自动响应。
- 进入页面输出
count.value ==> 0
- 一秒后自动输出
count.value ==> 1
然而,我们初次实现时遇到了问题:无法正确取值 (undefined
),也无法在值变更后触发更新。
为了解决这个问题,我们要思考 ref
需要做些什么:
- 当获取值时,
ref
要怎么知道是谁在读取它? - 当触发更新后,
ref
又要怎么知道该通知谁?
让 Ref 知道谁在读取
TypeScript
// 原始代码
class RefImpl {
_value;
constructor(value){
this._value = value
}
}
现在要加入 getter 和 setter,让 count.value
能正常运作:
TypeScript
class RefImpl {
_value;
constructor(value){
this._value = value
}
// 新增 getter:读取 value 时触发
get value(){
console.log('有人读取了 value!')
return this._value
}
// 新增 setter:设置 value 时触发
set value(newValue){
console.log('有人修改了 value!')
this._value = newValue
}
}

现在 count.value
看起来可以正常返回值了,但此时它还是不知道是谁在读取、需要通知谁。
Effect 函数
TypeScript
export function effect(fn){
fn()
}
这时候我们需要一个地方来存储当前正在执行的 effect 函数。
TypeScript
// effect.ts
// 用于保存当前正在执行的 effect 函数
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
这个新版的 effect
函数做了三件事:
- 注册副作用: 在执行传入的函数
fn
之前,先将它赋值给全局变量activeSub
。 - 执行副作用: 立即执行
fn()
。如果在执行过程中读取了某个ref
的.value
,这个ref
就能通过activeSub
知道是谁在读取它。 - 清除副作用: 执行完毕后,必须将
activeSub
清空 (设为undefined
)。这一点非常重要,它能确保只有在effect
的执行期间,读取ref
的行为才会被视为依赖收集。
收集依赖实现
现在我们要让 ref
能够:
- 在被读取时,记录是谁在读取(依赖收集)
- 在被修改时,通知所有读取者(触发更新)
我们可以在 getter 读取值的时候,判断 activeSub
是否存在,来确认当前情况是否需要收集依赖。
TypeScript
// ref.ts
import { activeSub } from './effect'
class RefImpl {
_value;
subs; // 新增:用于存储订阅者
constructor(value){
this._value = value
}
// 新增 getter:读取 value 时触发
get value(){
// 依赖收集:如果存在 activeSub,就记录下来
if(activeSub){
this.subs = activeSub
}
return this._value
}
// 新增 setter:设置 value 时触发
set value(newValue){
// 触发更新:如果存在订阅者,就执行它
if(this.subs){
this.subs() // 重新执行 effect
} // 可简写为 this.subs?.()
}
}
为了方便在后续的系统中判断一个变量是否为 ref
对象,我们可以新增一个辅助函数 isRef
和一个内部标记:
TypeScript
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value;
subs; // 新增:用于存储订阅者
[ReactiveFlags.IS_REF] = true
...
}
export function isRef(value){
return !!(value && value[ReactiveFlags.IS_REF])
}
现在,让我们将所有部分串联起来,完整地模拟一遍执行流程。
完整执行流程
页面初始化与依赖收集
刚开始进入页面。
JavaScript
import { ref, effect } from '../dist/reactivity.esm.js'
const count = ref(0)
程序执行:const count = ref(0)
-
执行
ref(0)
,创建一个RefImpl
实例。 -
此时
count
实例的内部状态为:_value: 0
- 没有任何订阅者:
subs: undefined
- 带有一个内部标记:
__v_isRef: true
调用 effect
函数,并传入匿名函数 fn
作为参数。
JavaScript
effect(() => {
console.log('effect', count.value)
})
进入 effect
函数内部
TypeScript
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
-
设置
activeSub
:activeSub
被赋值为fn
,即activeSub = fn
。 -
立即执行
fn()
-
执行
console.log('effect', count.value)
。 -
这触发了
count
实例的get value()
。 -
进入
getter
内部:-
if(activeSub)
条件成立,因为activeSub
正是我们的fn
。JavaScript
iniif(activeSub){ this.subs = activeSub }
-
-
执行"依赖收集" :
this.subs = activeSub
。 -
现在
count
实例通过subs
属性,记住了是fn
在依赖它。 -
getter
返回this._value
(也就是 0)。 -
console.log
输出:effect 0
。
-
-
activeSub = undefined
(执行完毕后清空,表示当前没有正在执行的 effect)。
此时:
count.subs
就是传入effect
的那个函数。- 依赖关系建立:
count
→effect(fn)
。
一秒之后
-
set value(newValue)
被调用,this._value = 1
。 -
this.subs?.()
被执行,即:如果存在订阅者,就调用它(这里就是前面存起来的effect
函数)。 -
触发更新:
effect
函数再次执行。console.log('effect', count.value)
→ 再次读取 getter。- 此时
activeSub
是undefined
,所以不会重复收集依赖。 - 这次是直接执行
effect
函数的本体,而不是 再次经过effect(fn)
的包装流程,所以第二次之后执行effect
时activeSub
是undefined
。 console.log
输出:effect 1
。
这样,我们就完成了响应式依赖收集的最小可行版本。
完整代码
ref.ts
TypeScript
import { activeSub } from './effect'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value; // 保存实际值
// ref 标记,证明这是一个 ref 对象
[ReactiveFlags.IS_REF] = true
subs
constructor(value){
this._value = value
}
// 收集依赖
get value(){
// 当有人访问时,可以获取 activeSub
if(activeSub){
// 当存在 activeSub 时存储它,以便更新后触发
this.subs = activeSub
}
return this._value
}
// 触发更新
set value(newValue){
this._value = newValue
// 通知 effect 重新执行,获取最新的 value
this.subs?.()
}
}
export function ref(value){
return new RefImpl(value)
}
export function isRef(value){
return !!(value && value[ReactiveFlags.IS_REF])
}
effect.ts
TypeScript
// 用于保存当前正在执行的 effect 函数
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。