茶杯狐
55.53MB · 2025-12-22
我们的代码虽然已经可以运作,但 RefImpl 这个类同时处理了数据存储和链表管理,不易于扩展,所以需要调整代码结构。尽管前几章的代码已经能正常工作,但它存在一个很大的问题:RefImpl 这个类承担了太多的责任。
它既要负责存储数值 (_value),又要管理一整套复杂的链表操作。
这种设计违反了软件工程中的 “单一职责原则 (Single Responsibility Principle)” ,会导致代码难以阅读、维护和扩展。
首先,我们把 RefImpl 中的链表操作抽离出来,创建两个独立的函数:
trackRef:收集依赖triggerRef:触发更新class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs: Link
subsTail: Link
constructor(value){
this._value = value
}
get value(){
if(activeSub){
trackRef(this)
}
return this._value
}
set value(newValue){
this._value = newValue
triggerRef(this)
}
}
/*
* 这里的 dep 是 ref
* 收集依赖,建立 ref 和 effect 之间的链表关系
*/
export function trackRef(dep){
const newLink = {
sub: activeSub,
nextSub: undefined,
prevSub: undefined
}
if(dep.subsTail){
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
} else {
dep.subs = newLink
dep.subsTail = newLink
}
}
/*
* 触发 ref 关联的 effect,使其重新执行
*/
export function triggerRef(dep){
let link = dep.subs
let queuedEffect = []
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect())
}
接着新建一个 system.ts 文件,用于存放链表相关逻辑,进行再次拆分:
trackRef:作为收集依赖的入口函数,判断是否存在 activeSub,如果存在,则建立链表关系。
effect(fn) 在调用 fn() 前会把自己设为 activeSub,在 fn() 结束后清空。所以我们使用 activeSub 来判断它是否为当前正在执行的 effect(fn)。triggerRef:作为触发更新的入口函数,需要找到并通知曾经订阅过这个 dep 的所有 effect。因此我们判断,如果 dep 存在 subs,它就触发更新。
dep (dependency) = 被依赖的对象(如 ref、reactive)
sub (subscriber) = 订阅者(如 effect、watch)
// system.ts
import { ReactiveEffect } from './effect'
export interface Link {
sub: ReactiveEffect
nextSub: Link
prevSub: Link
}
/* * 建立链表关系
* dep 是依赖项,例如 ref/computed/reactive
* sub 是订阅者,例如 effect
* 当依赖项(ref)变化时,需要通知订阅者(effect)
*/
export function link(dep, sub){
// 建立新的链表节点
const newLink: Link = {
sub, // 指向目前的订阅者 (activeSub)
nextSub: undefined, // 指向下一个节点 (初始化为空)
prevSub: undefined // 指向前一个节点 (初始化为空)
}
// 如果 dep 已经有尾端订阅者 (代表链表不是空的)
if(dep.subsTail){
// 把尾端节点的 next 指向新的节点
dep.subsTail.nextSub = newLink
// 新节点的 prev 指向原本的尾端
newLink.prevSub = dep.subsTail
// 更新 dep 的尾端指标为新节点
dep.subsTail = newLink
} else {
// 如果 dep 还没有任何订阅者 (第一次建立链表)
dep.subs = newLink // 链表的头指向新节点
dep.subsTail = newLink // 链表的尾也指向新节点
}
}
/* * 传播更新的函数
*/
export function propagate(subs){
let link = subs
let queuedEffect = []
while (link){
queuedEffect.push(link.sub)
link = link.nextSub
}
queuedEffect.forEach(effect => effect.run())
}
// ref.ts
import { activeSub, ReactiveEffect } from './effect'
import { Link, link, propagate } from './system'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl {
_value;
[ReactiveFlags.IS_REF] = true
subs: Link
subsTail: Link
constructor(value){
this._value = value
}
get value(){
if(activeSub){
trackRef(this)
}
return this._value
}
set value(newValue){
this._value = newValue
triggerRef(this)
}
}
export function ref(value){
return new RefImpl(value)
}
export function isRef(value){
return !!(value && value[ReactiveFlags.IS_REF])
}
/*
* 这里的 dep 是 ref
* 收集依赖,建立 ref 和 effect 之间的链表关系
*/
export function trackRef(dep){
if(activeSub){
link(dep, activeSub)
}
}
/*
* 触发 ref 关联的 effect,使其重新执行
*/
export function triggerRef(dep){
if(dep.subs){
propagate(dep.subs)
}
}
// effect.ts
// 用于保存当前正在执行的 effect
export let activeSub;
export function effect(fn){
activeSub = fn
activeSub()
activeSub = undefined
}
我们将 effect 函数重构为一个类,并给它一个 run 方法:
// effect.ts
import { Link } from './system'
export let activeSub: ReactiveEffect;
export class ReactiveEffect {
constructor(public fn: Function) {}
run(){
// 每次执行 fn 之前,把 this 实例放到 activeSub 上
activeSub = this
try {
return this.fn()
} finally {
// 执行完毕后,清空 activeSub
activeSub = undefined
}
}
}
export function effect(fn){
const e = new ReactiveEffect(fn)
e.run()
}
effect 重构为 ReactiveEffect 类?主要有三大好处:
effect 本身其实是有状态的(例如它依赖了谁、是否正在执行等)。类是封装这些状态和相关行为的最好办法。effect 成为一个类后,当有需要时,我们可以轻松地为它新增更多方法,比如 run() 就是一个很好的例子。this 指向:在 run() 方法中,activeSub 被赋值为 this (也就是 ReactiveEffect 的实例),方便后续我们从 effect 实例上获取更多需要的信息。也正因如此,effect 从函数变成了对象,所以我们要调整一下调用方式。
// system.ts
import { ReactiveEffect } from './effect'
export interface Link {
// 由于调整,effect 是对象
sub: ReactiveEffect
nextSub: Link
prevSub: Link
}
//...
//...
export function propagate(subs){
//...
// effect 变成了对象,改调用其 run 方法
queuedEffect.forEach(effect => effect.run())
}
回顾我们今天完成的事:我们将 RefImpl 中复杂的依赖追踪逻辑,拆分到了独立的 system.ts 模块,并且把 effect 重构成一个更易于维护的 ReactiveEffect 类。
现在,我们的响应式核心由三部分组成:RefImpl (负责数据内容)、ReactiveEffect (负责副作用)、以及 system.ts (连接它们的桥梁)。
明天我们就可以开始处理 effect 相关的其他问题了。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。