第1篇(Ref):搞定 Vue3 Reactivity 响应式源码
一步步,用 Proxy
+ 发布订阅 模式,手写 vue3/core/reactivity
实现 ref
源码中的 Ref
是一个工厂函数:
export function ref(value) {
return RefImpl(value)
}
具体的实现在RefImpl
里,它有几个特点:
-
value
是 普通值(Primitive) 和 对象(Object) 时,实现有所不同,因为对象有许多属性,有时要单独订阅对某些属性,value 是对象时交给reactivity
处理 - 在访问(get)时“收集依赖”,在赋值(set)时“触发依赖”
- 通过
RefImpl.value
访问值
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
;
而在一次重构中,又使用双向链表实现了他俩,所以接口长这样:
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
function effect(fn) {
const e = new ReactiveEffect(fn)
e.run()
const runner = () => e.run()
runner.effect = e
return runner
}
这里同样是工厂函数,好处是分层解耦,代码很灵活;
接下来实现 ReactiveEffect
,有个问题:到底 Dep
要被哪个 Sub
收集?
答案是 当前运行的 Sub
,我们用一个变量记录:
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
:
// 这里的 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
:
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
:
export function startTrack(sub: sub) {
// 标记正在收集依赖
sub.tracking = true
sub.depsTail = undefined
}
开始追踪:
-
tracking
标记当前sub
正在收集依赖,避免循环依赖问题; -
sub.depsTail
设置为空,为后面清理依赖做准备
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
来精准定位需要清理的范围