将依赖收集和派发更新从 RefImpl 中抽离出来,让 ref 只负责保存值
上一篇已经用双向链表解决了多个 effect 被覆盖的问题。
不过现在的 RefImpl 还是承担了太多职责:
- 保存实际值
- 收集依赖
- 维护链表
- 派发更新
这些逻辑都挤在 get value 和 set value 里,代码会越来越难维护。
这一篇把依赖收集和派发更新拆出去,让 RefImpl 只保留和 ref 自身强相关的逻辑。
重构目标
让每个模块只做一件事。
RefImpl:保存值,并在读取和修改时触发依赖收集和更新派发trackRef:负责依赖收集triggerRef:负责派发更新system.ts:负责底层链表操作
抽离 trackRef 和 triggerRef
先把 RefImpl 里的链表逻辑挪到两个函数里:
interface ReactiveNode {
subs?: Link
subsTail?: Link | undefined
}
class RefImpl implements ReactiveNode {
[ReactiveFlags.IS_REF] = true
subs: Link | undefined
subsTail: Link | undefined
constructor(value) {
this._value = value
}
_value
get value() {
if (activeSub) {
trackRef(this)
}
return this._value
}
set value(newValue) {
if (Object.is(newValue, this._value)) {
return
}
this._value = newValue
triggerRef(this)
}
}
RefImpl 不再直接关心链表怎么追加,也不再关心订阅者怎么执行。
读取时调用 trackRef(this),修改时调用 triggerRef(this):
/**
* 创建 ref 和 effect 之间的订阅关系
*/
export function trackRef(dep: ReactiveNode) {
const newLink = {
sub: activeSub,
prevSub: undefined,
nextSub: 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: ReactiveNode) {
const queuedEffects = []
let link = dep.subs
while (link) {
queuedEffects.push(link.sub)
link = link.nextSub
}
queuedEffects.forEach(effect => effect())
}
这样已经比上一篇更清晰了,但 trackRef 和 triggerRef 里仍然包含大量链表细节。
后续 reactive 也要收集依赖,也会需要同样的追加和遍历逻辑,所以还可以继续拆。
抽离 system.ts
新建一个 system.ts,专门存放依赖系统里的通用结构和链表操作:
// packages/reactivity/src/system.ts
export interface Link {
sub: Function
prevSub: Link | undefined
nextSub: Link | undefined
}
export interface ReactiveNode {
subs?: Link
subsTail?: Link | undefined
}
export function link(dep: ReactiveNode, sub: Function) {
const newLink: Link = {
sub,
prevSub: undefined,
nextSub: undefined
}
if (dep.subsTail) {
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
}
else {
dep.subs = newLink
dep.subsTail = newLink
}
}
export function propagate(subs: Link) {
const queuedEffects = []
let link: Link | undefined = subs
while (link) {
queuedEffects.push(link.sub)
link = link.nextSub
}
queuedEffects.forEach(effect => effect())
}
这里的两个函数职责更底层:
link:把订阅者追加到依赖节点上propagate:从头节点开始遍历,并执行收集到的订阅者
system.ts 不关心这个依赖节点来自 ref 还是其它响应式对象。
它只处理依赖节点和订阅者之间的关系。
改造 ref.ts
有了 system.ts 后,ref.ts 里的逻辑就可以继续简化:
// packages/reactivity/src/ref.ts
import type { Link, ReactiveNode } from './system'
import { activeSub } from './effect'
import { link, propagate } from './system'
enum ReactiveFlags {
IS_REF = '__v_isRef'
}
class RefImpl<T> implements ReactiveNode {
[ReactiveFlags.IS_REF] = true
private _value: T
subs: Link | undefined
subsTail: Link | undefined
constructor(value: T) {
this._value = value
}
get value() {
if (activeSub) {
trackRef(this)
}
return this._value
}
set value(newValue) {
if (Object.is(newValue, this._value)) {
return
}
this._value = newValue
triggerRef(this)
}
}
export function ref<T>(value: T) {
return new RefImpl(value)
}
export function trackRef(dep: ReactiveNode) {
if (activeSub) {
link(dep, activeSub)
}
}
export function triggerRef(dep: ReactiveNode) {
if (dep.subs) {
propagate(dep.subs)
}
}
现在 trackRef 和 triggerRef 也只剩下和 ref 相关的入口逻辑:
- 有当前激活的订阅者时,才调用
link - 当前节点有订阅者时,才调用
propagate
真正的链表操作已经被隔离到 system.ts 里。
将 effect 改成 ReactiveEffect
现在还有一个问题:订阅者 sub 保存的是普通函数。
普通函数可以执行,但它本身很难承载更多状态。 比如:
- 这个
effect当前是否正在执行 - 这个
effect依赖了哪些响应式数据
所以把 effect 包装成一个类:
// packages/reactivity/src/effect.ts
export let activeSub: ReactiveEffect | undefined
export class ReactiveEffect {
constructor(public fn) {
}
run() {
activeSub = this
try {
return this.fn()
}
finally {
activeSub = undefined
}
}
}
export function effect(fn: Function) {
const e = new ReactiveEffect(fn)
e.run()
}
之前的 activeSub 是一个函数,现在它变成了一个 ReactiveEffect 实例。
对应地,派发更新时也要从直接调用函数,改成调用实例上的 run:
export function propagate(subs: Link) {
const queuedEffects: ReactiveEffect[] = []
let link: Link | undefined = subs
while (link) {
queuedEffects.push(link.sub)
link = link.nextSub
}
queuedEffects.forEach(effect => effect.run())
}
这样 effect 就不只是一个可执行函数了,而是一个可持续扩展的类