perf(reactivity): use bitwise dep markers to optimize re-tracking (#4017)
This commit is contained in:
parent
cc09772d55
commit
6cf2377cd4
@ -8,7 +8,8 @@ import {
|
|||||||
DebuggerEvent,
|
DebuggerEvent,
|
||||||
markRaw,
|
markRaw,
|
||||||
shallowReactive,
|
shallowReactive,
|
||||||
readonly
|
readonly,
|
||||||
|
ReactiveEffectRunner
|
||||||
} from '../src/index'
|
} from '../src/index'
|
||||||
import { ITERATE_KEY } from '../src/effect'
|
import { ITERATE_KEY } from '../src/effect'
|
||||||
|
|
||||||
@ -490,6 +491,96 @@ describe('reactivity/effect', () => {
|
|||||||
expect(conditionalSpy).toHaveBeenCalledTimes(2)
|
expect(conditionalSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should handle deep effect recursion using cleanup fallback', () => {
|
||||||
|
const results = reactive([0])
|
||||||
|
const effects: { fx: ReactiveEffectRunner; index: number }[] = []
|
||||||
|
for (let i = 1; i < 40; i++) {
|
||||||
|
;(index => {
|
||||||
|
const fx = effect(() => {
|
||||||
|
results[index] = results[index - 1] * 2
|
||||||
|
})
|
||||||
|
effects.push({ fx, index })
|
||||||
|
})(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(results[39]).toBe(0)
|
||||||
|
results[0] = 1
|
||||||
|
expect(results[39]).toBe(Math.pow(2, 39))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should register deps independently during effect recursion', () => {
|
||||||
|
const input = reactive({ a: 1, b: 2, c: 0 })
|
||||||
|
const output = reactive({ fx1: 0, fx2: 0 })
|
||||||
|
|
||||||
|
const fx1Spy = jest.fn(() => {
|
||||||
|
let result = 0
|
||||||
|
if (input.c < 2) result += input.a
|
||||||
|
if (input.c > 1) result += input.b
|
||||||
|
output.fx1 = result
|
||||||
|
})
|
||||||
|
|
||||||
|
const fx1 = effect(fx1Spy)
|
||||||
|
|
||||||
|
const fx2Spy = jest.fn(() => {
|
||||||
|
let result = 0
|
||||||
|
if (input.c > 1) result += input.a
|
||||||
|
if (input.c < 3) result += input.b
|
||||||
|
output.fx2 = result + output.fx1
|
||||||
|
})
|
||||||
|
|
||||||
|
const fx2 = effect(fx2Spy)
|
||||||
|
|
||||||
|
expect(fx1).not.toBeNull()
|
||||||
|
expect(fx2).not.toBeNull()
|
||||||
|
|
||||||
|
expect(output.fx1).toBe(1)
|
||||||
|
expect(output.fx2).toBe(2 + 1)
|
||||||
|
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
fx1Spy.mockClear()
|
||||||
|
fx2Spy.mockClear()
|
||||||
|
input.b = 3
|
||||||
|
expect(output.fx1).toBe(1)
|
||||||
|
expect(output.fx2).toBe(3 + 1)
|
||||||
|
expect(fx1Spy).toHaveBeenCalledTimes(0)
|
||||||
|
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
fx1Spy.mockClear()
|
||||||
|
fx2Spy.mockClear()
|
||||||
|
input.c = 1
|
||||||
|
expect(output.fx1).toBe(1)
|
||||||
|
expect(output.fx2).toBe(3 + 1)
|
||||||
|
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
fx1Spy.mockClear()
|
||||||
|
fx2Spy.mockClear()
|
||||||
|
input.c = 2
|
||||||
|
expect(output.fx1).toBe(3)
|
||||||
|
expect(output.fx2).toBe(1 + 3 + 3)
|
||||||
|
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Invoked twice due to change of fx1.
|
||||||
|
expect(fx2Spy).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
fx1Spy.mockClear()
|
||||||
|
fx2Spy.mockClear()
|
||||||
|
input.c = 3
|
||||||
|
expect(output.fx1).toBe(3)
|
||||||
|
expect(output.fx2).toBe(1 + 3)
|
||||||
|
expect(fx1Spy).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
fx1Spy.mockClear()
|
||||||
|
fx2Spy.mockClear()
|
||||||
|
input.a = 10
|
||||||
|
expect(output.fx1).toBe(3)
|
||||||
|
expect(output.fx2).toBe(10 + 3)
|
||||||
|
expect(fx1Spy).toHaveBeenCalledTimes(0)
|
||||||
|
expect(fx2Spy).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should not double wrap if the passed function is a effect', () => {
|
it('should not double wrap if the passed function is a effect', () => {
|
||||||
const runner = effect(() => {})
|
const runner = effect(() => {})
|
||||||
const otherRunner = effect(runner)
|
const otherRunner = effect(runner)
|
||||||
|
51
packages/reactivity/src/Dep.ts
Normal file
51
packages/reactivity/src/Dep.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { ReactiveEffect, getTrackOpBit } from './effect'
|
||||||
|
|
||||||
|
export type Dep = Set<ReactiveEffect> & TrackedMarkers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wasTracked and newTracked maintain the status for several levels of effect
|
||||||
|
* tracking recursion. One bit per level is used to define wheter the dependency
|
||||||
|
* was/is tracked.
|
||||||
|
*/
|
||||||
|
type TrackedMarkers = { wasTracked: number; newTracked: number }
|
||||||
|
|
||||||
|
export function createDep(effects?: ReactiveEffect[]): Dep {
|
||||||
|
const dep = new Set<ReactiveEffect>(effects) as Dep
|
||||||
|
dep.wasTracked = 0
|
||||||
|
dep.newTracked = 0
|
||||||
|
return dep
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wasTracked(dep: Dep): boolean {
|
||||||
|
return hasBit(dep.wasTracked, getTrackOpBit())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newTracked(dep: Dep): boolean {
|
||||||
|
return hasBit(dep.newTracked, getTrackOpBit())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setWasTracked(dep: Dep) {
|
||||||
|
dep.wasTracked = setBit(dep.wasTracked, getTrackOpBit())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNewTracked(dep: Dep) {
|
||||||
|
dep.newTracked = setBit(dep.newTracked, getTrackOpBit())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetTracked(dep: Dep) {
|
||||||
|
const trackOpBit = getTrackOpBit()
|
||||||
|
dep.wasTracked = clearBit(dep.wasTracked, trackOpBit)
|
||||||
|
dep.newTracked = clearBit(dep.newTracked, trackOpBit)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBit(value: number, bit: number): boolean {
|
||||||
|
return (value & bit) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBit(value: number, bit: number): number {
|
||||||
|
return value | bit
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearBit(value: number, bit: number): number {
|
||||||
|
return value & ~bit
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { ReactiveEffect } from './effect'
|
|||||||
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
import { Ref, trackRefValue, triggerRefValue } from './ref'
|
||||||
import { isFunction, NOOP } from '@vue/shared'
|
import { isFunction, NOOP } from '@vue/shared'
|
||||||
import { ReactiveFlags, toRaw } from './reactive'
|
import { ReactiveFlags, toRaw } from './reactive'
|
||||||
|
import { Dep } from './Dep'
|
||||||
|
|
||||||
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
|
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
|
||||||
readonly value: T
|
readonly value: T
|
||||||
@ -30,7 +31,7 @@ export const setComputedScheduler = (s: ComputedScheduler | undefined) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ComputedRefImpl<T> {
|
class ComputedRefImpl<T> {
|
||||||
public dep?: Set<ReactiveEffect> = undefined
|
public dep?: Dep = undefined
|
||||||
|
|
||||||
private _value!: T
|
private _value!: T
|
||||||
private _dirty = true
|
private _dirty = true
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
|
import { extend, isArray, isIntegerKey, isMap } from '@vue/shared'
|
||||||
import { EffectScope, recordEffectScope } from './effectScope'
|
import { EffectScope, recordEffectScope } from './effectScope'
|
||||||
|
import {
|
||||||
|
createDep,
|
||||||
|
Dep,
|
||||||
|
newTracked,
|
||||||
|
resetTracked,
|
||||||
|
setNewTracked,
|
||||||
|
setWasTracked,
|
||||||
|
wasTracked
|
||||||
|
} from './Dep'
|
||||||
|
|
||||||
// The main WeakMap that stores {target -> key -> dep} connections.
|
// The main WeakMap that stores {target -> key -> dep} connections.
|
||||||
// Conceptually, it's easier to think of a dependency as a Dep class
|
// 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
|
// which maintains a Set of subscribers, but we simply store them as
|
||||||
// raw Sets to reduce memory overhead.
|
// raw Sets to reduce memory overhead.
|
||||||
type Dep = Set<ReactiveEffect>
|
|
||||||
type KeyToDepMap = Map<any, Dep>
|
type KeyToDepMap = Map<any, Dep>
|
||||||
const targetMap = new WeakMap<any, KeyToDepMap>()
|
const targetMap = new WeakMap<any, KeyToDepMap>()
|
||||||
|
|
||||||
@ -56,16 +64,54 @@ export class ReactiveEffect<T = any> {
|
|||||||
return this.fn()
|
return this.fn()
|
||||||
}
|
}
|
||||||
if (!effectStack.includes(this)) {
|
if (!effectStack.includes(this)) {
|
||||||
this.cleanup()
|
|
||||||
try {
|
try {
|
||||||
enableTracking()
|
|
||||||
effectStack.push((activeEffect = this))
|
effectStack.push((activeEffect = this))
|
||||||
|
enableTracking()
|
||||||
|
|
||||||
|
effectTrackDepth++
|
||||||
|
|
||||||
|
if (effectTrackDepth <= maxMarkerBits) {
|
||||||
|
this.initDepMarkers()
|
||||||
|
} else {
|
||||||
|
this.cleanup()
|
||||||
|
}
|
||||||
return this.fn()
|
return this.fn()
|
||||||
} finally {
|
} finally {
|
||||||
effectStack.pop()
|
if (effectTrackDepth <= maxMarkerBits) {
|
||||||
resetTracking()
|
this.finalizeDepMarkers()
|
||||||
activeEffect = effectStack[effectStack.length - 1]
|
|
||||||
}
|
}
|
||||||
|
effectTrackDepth--
|
||||||
|
resetTracking()
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 {
|
export interface ReactiveEffectOptions {
|
||||||
lazy?: boolean
|
lazy?: boolean
|
||||||
scheduler?: EffectScheduler
|
scheduler?: EffectScheduler
|
||||||
@ -158,7 +218,8 @@ export function track(target: object, type: TrackOpTypes, key: unknown) {
|
|||||||
}
|
}
|
||||||
let dep = depsMap.get(key)
|
let dep = depsMap.get(key)
|
||||||
if (!dep) {
|
if (!dep) {
|
||||||
depsMap.set(key, (dep = new Set()))
|
dep = createDep()
|
||||||
|
depsMap.set(key, dep)
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventInfo = __DEV__
|
const eventInfo = __DEV__
|
||||||
@ -173,10 +234,21 @@ export function isTracking() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function trackEffects(
|
export function trackEffects(
|
||||||
dep: Set<ReactiveEffect>,
|
dep: Dep,
|
||||||
debuggerEventExtraInfo?: DebuggerEventExtraInfo
|
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!)
|
dep.add(activeEffect!)
|
||||||
activeEffect!.deps.push(dep)
|
activeEffect!.deps.push(dep)
|
||||||
if (__DEV__ && activeEffect!.onTrack) {
|
if (__DEV__ && activeEffect!.onTrack) {
|
||||||
@ -267,7 +339,7 @@ export function trigger(
|
|||||||
effects.push(...dep)
|
effects.push(...dep)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
triggerEffects(new Set(effects), eventInfo)
|
triggerEffects(createDep(effects), eventInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import {
|
import { isTracking, trackEffects, triggerEffects } from './effect'
|
||||||
isTracking,
|
|
||||||
ReactiveEffect,
|
|
||||||
trackEffects,
|
|
||||||
triggerEffects
|
|
||||||
} from './effect'
|
|
||||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||||
import { isArray, isObject, hasChanged } from '@vue/shared'
|
import { isArray, isObject, hasChanged } from '@vue/shared'
|
||||||
import { reactive, isProxy, toRaw, isReactive } from './reactive'
|
import { reactive, isProxy, toRaw, isReactive } from './reactive'
|
||||||
import { CollectionTypes } from './collectionHandlers'
|
import { CollectionTypes } from './collectionHandlers'
|
||||||
|
import { createDep, Dep } from './Dep'
|
||||||
|
|
||||||
export declare const RefSymbol: unique symbol
|
export declare const RefSymbol: unique symbol
|
||||||
|
|
||||||
@ -27,11 +23,11 @@ export interface Ref<T = any> {
|
|||||||
/**
|
/**
|
||||||
* Deps are maintained locally rather than in depsMap for performance reasons.
|
* Deps are maintained locally rather than in depsMap for performance reasons.
|
||||||
*/
|
*/
|
||||||
dep?: Set<ReactiveEffect>
|
dep?: Dep
|
||||||
}
|
}
|
||||||
|
|
||||||
type RefBase<T> = {
|
type RefBase<T> = {
|
||||||
dep?: Set<ReactiveEffect>
|
dep?: Dep
|
||||||
value: T
|
value: T
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +35,7 @@ export function trackRefValue(ref: RefBase<any>) {
|
|||||||
if (isTracking()) {
|
if (isTracking()) {
|
||||||
ref = toRaw(ref)
|
ref = toRaw(ref)
|
||||||
if (!ref.dep) {
|
if (!ref.dep) {
|
||||||
ref.dep = new Set<ReactiveEffect>()
|
ref.dep = createDep()
|
||||||
}
|
}
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
trackEffects(ref.dep, {
|
trackEffects(ref.dep, {
|
||||||
@ -104,7 +100,7 @@ class RefImpl<T> {
|
|||||||
private _value: T
|
private _value: T
|
||||||
private _rawValue: T
|
private _rawValue: T
|
||||||
|
|
||||||
public dep?: Set<ReactiveEffect> = undefined
|
public dep?: Dep = undefined
|
||||||
public readonly __v_isRef = true
|
public readonly __v_isRef = true
|
||||||
|
|
||||||
constructor(value: T, public readonly _shallow = false) {
|
constructor(value: T, public readonly _shallow = false) {
|
||||||
@ -172,7 +168,7 @@ export type CustomRefFactory<T> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CustomRefImpl<T> {
|
class CustomRefImpl<T> {
|
||||||
public dep?: Set<ReactiveEffect> = undefined
|
public dep?: Dep = undefined
|
||||||
|
|
||||||
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
|
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
|
||||||
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
|
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
|
||||||
|
@ -1395,12 +1395,16 @@ function baseCreateRenderer(
|
|||||||
isSVG,
|
isSVG,
|
||||||
optimized
|
optimized
|
||||||
) => {
|
) => {
|
||||||
const componentUpdateFn = () => {
|
const componentUpdateFn = function(this: ReactiveEffect) {
|
||||||
if (!instance.isMounted) {
|
if (!instance.isMounted) {
|
||||||
let vnodeHook: VNodeHook | null | undefined
|
let vnodeHook: VNodeHook | null | undefined
|
||||||
const { el, props } = initialVNode
|
const { el, props } = initialVNode
|
||||||
const { bm, m, parent } = instance
|
const { bm, m, parent } = instance
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disallow component effect recursion during pre-lifecycle hooks.
|
||||||
|
this.allowRecurse = false
|
||||||
|
|
||||||
// beforeMount hook
|
// beforeMount hook
|
||||||
if (bm) {
|
if (bm) {
|
||||||
invokeArrayFns(bm)
|
invokeArrayFns(bm)
|
||||||
@ -1415,6 +1419,9 @@ function baseCreateRenderer(
|
|||||||
) {
|
) {
|
||||||
instance.emit('hook:beforeMount')
|
instance.emit('hook:beforeMount')
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.allowRecurse = true
|
||||||
|
}
|
||||||
|
|
||||||
if (el && hydrateNode) {
|
if (el && hydrateNode) {
|
||||||
// vnode has adopted host node - perform hydration instead of mount.
|
// vnode has adopted host node - perform hydration instead of mount.
|
||||||
@ -1540,6 +1547,10 @@ function baseCreateRenderer(
|
|||||||
next = vnode
|
next = vnode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disallow component effect recursion during pre-lifecycle hooks.
|
||||||
|
this.allowRecurse = false
|
||||||
|
|
||||||
// beforeUpdate hook
|
// beforeUpdate hook
|
||||||
if (bu) {
|
if (bu) {
|
||||||
invokeArrayFns(bu)
|
invokeArrayFns(bu)
|
||||||
@ -1554,6 +1565,9 @@ function baseCreateRenderer(
|
|||||||
) {
|
) {
|
||||||
instance.emit('hook:beforeUpdate')
|
instance.emit('hook:beforeUpdate')
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.allowRecurse = true
|
||||||
|
}
|
||||||
|
|
||||||
// render
|
// render
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
|
Loading…
Reference in New Issue
Block a user