1.订阅-发布
Dep
类是 vue2
中用于管理依赖的类。
该类主要利用订阅-发布模式实现了管理依赖的添加、删除和通知更新。
其中,Dep
类中维护了一个 subs
数组,用于存储依赖。
addSub
方法用于添加依赖;removeSub
方法用于删除依赖;notify
方法用于通知依赖更新。depend
方法用于将依赖实例添加到Watcher
中(Dep.target
对应的是Watcher
实例)。
其中,addSub
和 notify
逻辑较清晰,无需其他说明。
下一节,我们重点分析 removeSub
和 depend
方法。
ts
class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget | null>
// pending subs cleanup
_pending = false
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
// #12696 deps with massive amount of subscribers are extremely slow to
// clean up in Chromium
// to workaround this, we unset the sub for now, and clear them on
// next scheduler flush.
this.subs[this.subs.indexOf(sub)] = null
if (!this._pending) {
this._pending = true
pendingCleanupDeps.push(this)
}
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
notify(info?: DebuggerEventExtraInfo) {
// stabilize the subscriber list first
const subs = this.subs.filter(s => s) as DepTarget[]
if (__DEV__ && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
const sub = subs[i]
if (__DEV__ && info) {
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
sub.update()
}
}
}
2. removeSub
removeSub
方法用于删除依赖。
针对性解决了 Chrome
浏览器中大量订阅者清理时的性能问题。
- 在
Chrome
中,当一个依赖(Dep
)有大量订阅者(subscribers
)时 - 直接使用
Array.splice()
或filter()
来移除订阅者会非常慢 - 这是
Chrome
浏览器的一个特定问题(issue #12696
)
解决方案
typescript
removeSub(sub: DepTarget) {
// 不直接从数组中移除元素,而是先将其设置为 null
this.subs[this.subs.indexOf(sub)] = null
// 如果还没有待清理标记
if (!this._pending) {
// 标记为待清理
this._pending = true
// 将这个 dep 添加到待清理队列
pendingCleanupDeps.push(this)
}
}
// 在下一个调度周期进行实际的清理
const cleanupDeps = () => {
for (let i = 0; i < pendingCleanupDeps.length; i++) {
const dep = pendingCleanupDeps[i]
// 过滤掉所有 null 值
dep.subs = dep.subs.filter(s => s)
dep._pending = false
}
// 清空待清理队列
pendingCleanupDeps.length = 0
}
假设有一个 dep
实例:
typescript
const dep = new Dep()
// 添加一些订阅者
const sub1 = { /* ... */ }
const sub2 = { /* ... */ }
dep.addSub(sub1)
dep.addSub(sub2)
// 移除订阅者
dep.removeSub(sub1)
// 此时 sub1 在数组中变成了 null,但还没有被真正移除
// [null, sub2]
// 在下一个调度周期
cleanupDeps()
// 此时才真正清理掉 null 值
// [sub2]
这种优化方案:
- 避免了频繁的数组操作
- 将清理操作批量化处理
- 显著提升了在
Chrome
中处理大量订阅者时的性能
3. depend
depend
方法用于将依赖实例添加到 Watcher
中(Dep.target
对应的是 Watcher
实例)。
ts
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
可以发现,核心作用是 Dep.target.addDep
方法。
而 Dep.target
的值,是通过 pushTarget
和 popTarget
方法设置的。
关于 pushTarget
和 popTarget
方法的逻辑:
ts
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
设计 pushTarget
和 popTarget
主要是为了处理嵌套的 watcher 场景。
javascript
const vm = {
computed: {
// 计算属性 A
computedA() {
// 这里访问了计算属性 B
return this.computedB + ' A'
},
// 计算属性 B
computedB() {
return this.message + ' B'
}
},
data: {
message: 'Hello'
}
}
如果直接操作 Dep.target
,会导致 watcher
被覆盖。
ts
class Watcher {
get() {
Dep.target = this // 直接设置
// 执行 getter
const value = this.getter.call(vm, vm)
Dep.target = null // 直接清除
return value
}
}
// 问题展示
computedA 的 watcher 执行:
Dep.target = watcherA
↓
执行 computedA 的 getter
↓
访问 computedB,触发 computedB 的 watcher
↓
Dep.target = watcherB // ⚠️ watcherA 被覆盖了!
↓
执行完 computedB
↓
Dep.target = null // ⚠️ watcherA 永远无法恢复!
使用 pushTarget
和 popTarget
的话:
ts
// 现在的执行流程
computedA 的 watcher 执行:
pushTarget(watcherA) // targetStack = [watcherA]
↓
执行 computedA 的 getter
↓
访问 computedB,触发 computedB 的 watcher
↓
pushTarget(watcherB) // targetStack = [watcherA, watcherB]
↓
执行完 computedB
↓
popTarget() // targetStack = [watcherA],恢复 watcherA
↓
继续执行 computedA
↓
popTarget() // targetStack = []