perf(reactivity): use bitwise dep markers to optimize re-tracking (#4017)

This commit is contained in:
Bas van Meurs
2021-07-07 20:13:23 +02:00
committed by Evan You
parent cc09772d55
commit 6cf2377cd4
6 changed files with 274 additions and 49 deletions

View File

@@ -1,12 +1,20 @@
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
import { EffectScope, recordEffectScope } from './effectScope'
import {
createDep,
Dep,
newTracked,
resetTracked,
setNewTracked,
setWasTracked,
wasTracked
} from './Dep'
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
@@ -56,19 +64,57 @@ export class ReactiveEffect<T = any> {
return this.fn()
}
if (!effectStack.includes(this)) {
this.cleanup()
try {
enableTracking()
effectStack.push((activeEffect = this))
enableTracking()
effectTrackDepth++
if (effectTrackDepth <= maxMarkerBits) {
this.initDepMarkers()
} else {
this.cleanup()
}
return this.fn()
} finally {
effectStack.pop()
if (effectTrackDepth <= maxMarkerBits) {
this.finalizeDepMarkers()
}
effectTrackDepth--
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n - 1] : undefined
}
}
}
initDepMarkers() {
const { deps } = this
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
setWasTracked(deps[i])
}
}
}
finalizeDepMarkers() {
const { deps } = this
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(this)
} else {
deps[ptr++] = dep
}
resetTracked(dep)
}
deps.length = ptr
}
}
cleanup() {
const { deps } = this
if (deps.length) {
@@ -90,6 +136,20 @@ export class ReactiveEffect<T = any> {
}
}
// The number of effects currently being tracked recursively.
let effectTrackDepth = 0
/**
* The bitwise track markers support at most 30 levels op recursion.
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
* When recursion depth is greater, fall back to using a full cleanup.
*/
const maxMarkerBits = 30
export function getTrackOpBit(): number {
return 1 << effectTrackDepth
}
export interface ReactiveEffectOptions {
lazy?: boolean
scheduler?: EffectScheduler
@@ -158,7 +218,8 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
dep = createDep()
depsMap.set(key, dep)
}
const eventInfo = __DEV__
@@ -173,10 +234,21 @@ export function isTracking() {
}
export function trackEffects(
dep: Set<ReactiveEffect>,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (!dep.has(activeEffect!)) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
setNewTracked(dep)
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
@@ -267,7 +339,7 @@ export function trigger(
effects.push(...dep)
}
}
triggerEffects(new Set(effects), eventInfo)
triggerEffects(createDep(effects), eventInfo)
}
}