一步步,用 Proxy + 发布订阅 模式,手写 vue3/core/reactivity
实现 ref
源码中的 Ref 是一个工厂函数:
js
export function ref(value) {
return RefImpl(value)
}
具体的实现在RefImpl里,它有几个特点:
value是 普通值(Primitive) 和 对象(Object) 时,实现有所不同,因为对象有许多属性,有时要单独订阅对某些属性,value 是对象时交给reactivity处理- 在访问(get )时"收集依赖",在赋值(set)时"触发依赖"
- 通过
RefImpl.value访问值
js
class RefImpl {
privite _value
constructor() {
// 暂时忽略 Object 情况
this._value = value
}
// trackRef 和 triggerRef 的实现放在后面
get value() {
trackRef(this)
return _value
}
set value(newValue) {
this._value = newValue
triggerRef(this)
}
}
这里要引入一些概念:
ref叫Dep,即被订阅者 、依赖effect叫Sub,即订阅者 、观察者
代码中把这两者抽象成了接口(interface),即 ref 实现了 Dep,effect 实现了 Sub; 而在一次重构中,又使用双向链表实现了他俩,所以接口长这样:
js
interface Sub {
// 头
subs: Link
// 尾
subsTail: Link
}
interface Dep {
// 头
deps: Link
// 尾
depsTail: Link
}
// 链表中 `链` 的抽象
// 直接连接 Dep 和 Sub
export interface Link {
sub: Sub
nextSub: Link | undefined
prevSub: Link | undefined
dep: Dep
nextDep: Link | undefined
}
然后,先实现一下 effect,再实现 trackRef 和 triggerRef; 在创建 effect 时,要先执行一遍函数,收集依赖,然后返回一个可以手动执行函数的 handler runner
js
function effect(fn) {
const e = new ReactiveEffect(fn)
e.run()
const runner = () => e.run()
runner.effect = e
return runner
}
这里同样是工厂函数,好处是分层解耦,代码很灵活; 接下来实现 ReactiveEffect,有个问题:到底 Dep 要被哪个 Sub 收集?
答案是 当前运行的 Sub,我们用一个变量记录:
js
export let activeSub;
export function setActiveSub(sub) {
activeSub = sub
}
class ReactiveEffect implement Sub {
deps: Link
depsTail: Link
// effect 有停止机制,即停止收集依赖
active = true
// 标记,防止递归依赖
tracking = false
constructor(public fn) {}
run() {
// 停止后,不再收集依赖
if (!this.active) return this.fn()
// effect 递归触发,要用闭包保存上一个
let prevSub = activeSub
activeSub = this
startTrack(this)
try {
return this.fn()
} finally {
endTrack(this)
activeSub = prevSub
}
}
}
这里的 stratTrack 和 endTrack 是为了在收集依赖时,复用实例&垃圾收集(删除不再依赖的项目),先埋个坑; 下面实现 trackRef 和 triggerRef:
js
// 这里的 dep 就是 ref
export function trackRef(dep) {
// 连接 Sub 和 Dep
if (activeSub) {
link(dep, activeSub)
}
}
// 用来存储已经断开连接的 Link 实例,可以复用,避免重复创建 Link
let LinkPool: Link | undefined
export function link(dep, sub) {
// 当前 sub 的最后一个 Dep
const currentDep = sub.depsTail
// 下一个 Dep
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// 尝试复用链表
if (nextDep && nextDep === dep) {
sub.depsTail = nextDep
return
}
// 不能复用,创建新的 Link
// 插入 sub 的 dep链表 中在当前 deps 链表中插入一个节点
let newLink: Link
if (LinkPool) {
newLink = LinkPool
LinkPool = LinkPool.nextDep
newLink.nextDep = nextDep
newLink.dep = dep
newLink.sub = sub
} else {
newLink = {
sub,
nextSub: undefined,
prevSub: undefined,
dep,
nextDep,
}
}
// 插入 dep 的 sub链表 中
// 连接到 subsTail 后面
if (dep.subsTail) {
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
} else {
// 还没有 sub,这是第一个
dep.subs = newLink
dep.subsTail = newLink
}
// 建立 deps 单向链表关系
if (sub.depsTail) {
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
} else {
sub.deps = newLink
sub.depsTail = newLink
}
}
上面有几个点注意:
dep是单链表,sub链表是双链表,因为:dep清理时只需要从某一点开始删除到末尾,而sub需要删除特定的节点;- 贪心策略:尝试直接使用
sub.depsTail.nextDep - 实例收集策略:使用
LinkPool收集可以重用的Link
然后是 triggerRef:
js
export function triggerRef(dep) {
// 仅当有订阅者时执行
if (dep.subs) {
// 委托
propagate(dep.subs)
}
}
// 传播更新,收集将要触发的 effect,
export propagate(subs: Link) {
let link = subs
const queuedEffect = []
while (link) {
const sub = link.sub
// 避免递归触发
if (!sub.tracking) {
queuedEffect.push(sub)
}
link = link.nextSub
}
// 触发所有 sub
queuedEffect.forEach(effect => effect.notify())
}
最后再填坑 startTrack 和 endTrack:
js
export function startTrack(sub: sub) {
// 标记正在收集依赖
sub.tracking = true
sub.depsTail = undefined
}
开始追踪:
tracking标记当前sub正在收集依赖,避免循环依赖问题;sub.depsTail设置为空,为后面清理依赖做准备
js
export endTrack(sub: Sub) {
sub.tracking = false
const depsTail = sub.depsTail
// 重新收集依赖后,depsTail 会指向新的最后一个依赖
if (depsTail && depsTail.next) {
// 部份清理,新的最后一个依赖后的,都是不再依赖的
clearTracking(depsTail.nextDep)
depsTail.nextDep = undefined
} else if (sub.deps) {
// depsTail 依旧是空,表示没有任何新的依赖
clearTracking(sub.deps)
sub.deps = undefined
}
}
// 清理包括 link 在内的节点
export function clearTracking(link: Link) {
while(link) {
const { prevSub, nextSub, dep, nextDep } = link
// 把 prevSub 接到 link.nextSub
if (prevSub) {
prevSub.nextSub = link.nextSub
link.nextSub = undefined
} else {
dep.subsTail = prevSub
}
// 把 nextSub.prevSub 接到 link.prevSub
if (nextSub) {
nextSub.prevSub = link.prevSub
link.prevSub = undefined
} else {
dep.subsTail = prevSub
}
// 清空 dep/sub
link.dep = link.sub = undefined
// 存入复用池
link.nextDep = LinkPool
LinkPool = link
link = nextDep
}
}
几个需要注意的点:
- 增量更新:只清理不再需要的,保留仍需要的
- 内存优化:复用
Link实例 - 双向链表:支持 O(1) 时间复杂度
- 精准清理:通过
depsTail来精准定位需要清理的范围