1. 依赖清理优化算法详解-算法思路
每次执行副作用函数前都会先清理所有依赖再重新收集,从而减少无效依赖的触发。但如下情况,这种清理工作反而增加无谓的性能消耗
const state = reactive({ show: true, values: [1,2,3] })
effect(() => {
console.log(state.values)
})
那么应该如何处理呢?@vue/reactivity已经为我们提供了一个非常优秀的解决方案,请看下面吧。
1.1. 逐行分析优化算法
export type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
/**
* wasTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否已被跟踪过(即在上一轮副作用函数执行时已经被访问过)
*/
w: number
/**
* newTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否为新增(即在本轮副作用函数执行中被访问过)
*/
n: number
}
export const createDep = (effects) => {
const dep = new Set<ReactiveEffect>(effects) as Dep
// 虽然TrackedMarkers标识是位于响应式对象属性的依赖集合上,但它每一位仅用于表示当前执行的副作用函数是否曾经访问和正在访问该响应式对象属性
dep.w = 0
dep.n = 0
return dep
}
export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
/**
* 将当前副作用函数的依赖标记为 `已经被收集`
*/
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit
}
}
}
/**
* 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
* 即,新跟踪的 和 本轮跟踪过的都会被保留。
*/
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
// 对于曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
dep.delete(effect)
}
else {
// 缩小依赖集合的大小
deps[ptr++] = dep
}
// 将w和n中对应的嵌套层级的二进制位置零,如果缺少这步后续副作用函数重新执行时则无法重新收集依赖。
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
// 缩小依赖集合的大小
deps.length = ptr
}
}
// 在位于响应式上下文执行的副作用函数内,访问响应式对象属性,将通过track收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return
}
// targetMap用于存储响应式对象-对象属性的键值对
// depsMap用于存储对象属性-副作用函数集合的键值对
let depsMap = targetMap.get(target)
if (!depsMap) {
target.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
trackEffects(dep)
}
// 收集依赖
export function trackEffects(
dep: Dep
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
// 如果本轮副作用函数执行过程中已经访问并收集过,则不用再收集该依赖
if (!newTracked(dep)) {
dep.n |= trackOpBit
shouldTrack = !wasTracked(dep)
}
}
else {
// 对于全面清理的情况,如果当前副作用函数对应的ReactiveEffect对象不在依赖集合中,则标记为true
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
}
}
单单从代码实现角度能难理解这个优化方式,不如我们从实际的例子出发吧!
const runAsync = fn => setTimeout(fn, 1000)
const state = reactive({ show: true, values: [1,2,3] })
// 1
effect(() => {
if (state.show) {
console.log(state.values)
}
})
// 2
runAsync(() => {
state.values.push(4)
})
// 3
runAsync(() => {
state.show = false
})
- 首次执行副作用函数
a.
effectTrackDepth
为0,因此1 << ++effectTrackDepth
得到的effectTrackDepth
和trackOpBit
均为1,但由于此时副作用函数还没有收集依赖,因此initDepMarkers
函数没有任何效果; b. 访问state.show
时由于之前没有收集过响应式对象state
的show
属性,因此会调用createDep
创建w
和n
均为0的依赖集合,并调用trackEffects
发现newTracked(dep)
为未跟踪过,则将n
设置为1,然后开始收集依赖; c. 访问state.values
会重复第2步的操作; d. 由于state.show
和state.values
都是新跟踪的(n
为1),因此在finalizeDepMarkers
处理后仍然将副作用函数保留在这两个属性对应的依赖集合中。 - 执行
state.values.push(4)
触发副作用函数变化 a.effectTrackDepth
为0,因此1 << ++effectTrackDepth
得到的effectTrackDepth
和trackOpBit
均为1,此时副作用函数已经收集过依赖,因此initDepMarkers
将该副作用函数所在的依赖集合都都标记为已收集过(w
为1); b. 访问state.show
时会调用trackEffects
发现newTracked(dep)
为未跟踪过(在finalizeDepMarkers
中已被置零),则将n
设置为1,然后开始收集依赖; c. 访问state.values
会重复第2步的操作; d. 由于state.show
和state.values
都是新跟踪的(n
为1),因此在finalizeDepMarkers
处理后仍然将副作用函数保留在这两个属性对应的依赖集合中。 - 执行
state.show = false
触发副作用函数变化 a.effectTrackDepth
为0,因此1 << ++effectTrackDepth
得到的effectTrackDepth
和trackOpBit
均为1,此时副作用函数已经收集过依赖,因此initDepMarkers
将该副作用函数所在的依赖集合都都标记为已收集过(w
为1); b. 访问state.show
时会调用trackEffects
发现newTracked(dep)
为未跟踪过(在finalizeDepMarkers
中已被置零),则将n
设置为1,然后开始收集依赖; c. 由于state.values
没有标记为新跟踪的(n
为0),因此在finalizeDepMarkers
处理后会将副作用函数从state.values
对应的依赖集合中移除,仅保留在state.values
对应的依赖集合中。
@vue/reactivity给我们展示了一个非常优秀的处理方式,那么就是通过标识每个依赖集合的状态(新依赖和已经被收集过),并对新依赖和已经被收集过两个标识进行对比筛选出已被删除的依赖项。
到这里,我想大家已经对这个优化有更深的理解了。那么接下来的问题自然而然就是为什么要硬编码将优化算法启动的嵌套层级设置为maxMarkerBits = 30
?
请看下一节《依赖清理优化算法详解-基于JavaScript引擎的SMI进一步压榨性能》