
前言
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」,一起跟日安当同学。