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,
|
||||
markRaw,
|
||||
shallowReactive,
|
||||
readonly
|
||||
readonly,
|
||||
ReactiveEffectRunner
|
||||
} from '../src/index'
|
||||
import { ITERATE_KEY } from '../src/effect'
|
||||
|
||||
@ -490,6 +491,96 @@ describe('reactivity/effect', () => {
|
||||
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', () => {
|
||||
const runner = effect(() => {})
|
||||
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 { isFunction, NOOP } from '@vue/shared'
|
||||
import { ReactiveFlags, toRaw } from './reactive'
|
||||
import { Dep } from './Dep'
|
||||
|
||||
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
|
||||
readonly value: T
|
||||
@ -30,7 +31,7 @@ export const setComputedScheduler = (s: ComputedScheduler | undefined) => {
|
||||
}
|
||||
|
||||
class ComputedRefImpl<T> {
|
||||
public dep?: Set<ReactiveEffect> = undefined
|
||||
public dep?: Dep = undefined
|
||||
|
||||
private _value!: T
|
||||
private _dirty = true
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
import {
|
||||
isTracking,
|
||||
ReactiveEffect,
|
||||
trackEffects,
|
||||
triggerEffects
|
||||
} from './effect'
|
||||
import { isTracking, trackEffects, triggerEffects } from './effect'
|
||||
import { TrackOpTypes, TriggerOpTypes } from './operations'
|
||||
import { isArray, isObject, hasChanged } from '@vue/shared'
|
||||
import { reactive, isProxy, toRaw, isReactive } from './reactive'
|
||||
import { CollectionTypes } from './collectionHandlers'
|
||||
import { createDep, Dep } from './Dep'
|
||||
|
||||
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.
|
||||
*/
|
||||
dep?: Set<ReactiveEffect>
|
||||
dep?: Dep
|
||||
}
|
||||
|
||||
type RefBase<T> = {
|
||||
dep?: Set<ReactiveEffect>
|
||||
dep?: Dep
|
||||
value: T
|
||||
}
|
||||
|
||||
@ -39,7 +35,7 @@ export function trackRefValue(ref: RefBase<any>) {
|
||||
if (isTracking()) {
|
||||
ref = toRaw(ref)
|
||||
if (!ref.dep) {
|
||||
ref.dep = new Set<ReactiveEffect>()
|
||||
ref.dep = createDep()
|
||||
}
|
||||
if (__DEV__) {
|
||||
trackEffects(ref.dep, {
|
||||
@ -104,7 +100,7 @@ class RefImpl<T> {
|
||||
private _value: T
|
||||
private _rawValue: T
|
||||
|
||||
public dep?: Set<ReactiveEffect> = undefined
|
||||
public dep?: Dep = undefined
|
||||
public readonly __v_isRef = true
|
||||
|
||||
constructor(value: T, public readonly _shallow = false) {
|
||||
@ -172,7 +168,7 @@ export type CustomRefFactory<T> = (
|
||||
}
|
||||
|
||||
class CustomRefImpl<T> {
|
||||
public dep?: Set<ReactiveEffect> = undefined
|
||||
public dep?: Dep = undefined
|
||||
|
||||
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
|
||||
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
|
||||
|
@ -1395,25 +1395,32 @@ function baseCreateRenderer(
|
||||
isSVG,
|
||||
optimized
|
||||
) => {
|
||||
const componentUpdateFn = () => {
|
||||
const componentUpdateFn = function(this: ReactiveEffect) {
|
||||
if (!instance.isMounted) {
|
||||
let vnodeHook: VNodeHook | null | undefined
|
||||
const { el, props } = initialVNode
|
||||
const { bm, m, parent } = instance
|
||||
|
||||
// beforeMount hook
|
||||
if (bm) {
|
||||
invokeArrayFns(bm)
|
||||
}
|
||||
// onVnodeBeforeMount
|
||||
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
|
||||
invokeVNodeHook(vnodeHook, parent, initialVNode)
|
||||
}
|
||||
if (
|
||||
__COMPAT__ &&
|
||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||
) {
|
||||
instance.emit('hook:beforeMount')
|
||||
try {
|
||||
// Disallow component effect recursion during pre-lifecycle hooks.
|
||||
this.allowRecurse = false
|
||||
|
||||
// beforeMount hook
|
||||
if (bm) {
|
||||
invokeArrayFns(bm)
|
||||
}
|
||||
// onVnodeBeforeMount
|
||||
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
|
||||
invokeVNodeHook(vnodeHook, parent, initialVNode)
|
||||
}
|
||||
if (
|
||||
__COMPAT__ &&
|
||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||
) {
|
||||
instance.emit('hook:beforeMount')
|
||||
}
|
||||
} finally {
|
||||
this.allowRecurse = true
|
||||
}
|
||||
|
||||
if (el && hydrateNode) {
|
||||
@ -1540,19 +1547,26 @@ function baseCreateRenderer(
|
||||
next = vnode
|
||||
}
|
||||
|
||||
// beforeUpdate hook
|
||||
if (bu) {
|
||||
invokeArrayFns(bu)
|
||||
}
|
||||
// onVnodeBeforeUpdate
|
||||
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
|
||||
invokeVNodeHook(vnodeHook, parent, next, vnode)
|
||||
}
|
||||
if (
|
||||
__COMPAT__ &&
|
||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||
) {
|
||||
instance.emit('hook:beforeUpdate')
|
||||
try {
|
||||
// Disallow component effect recursion during pre-lifecycle hooks.
|
||||
this.allowRecurse = false
|
||||
|
||||
// beforeUpdate hook
|
||||
if (bu) {
|
||||
invokeArrayFns(bu)
|
||||
}
|
||||
// onVnodeBeforeUpdate
|
||||
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
|
||||
invokeVNodeHook(vnodeHook, parent, next, vnode)
|
||||
}
|
||||
if (
|
||||
__COMPAT__ &&
|
||||
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
|
||||
) {
|
||||
instance.emit('hook:beforeUpdate')
|
||||
}
|
||||
} finally {
|
||||
this.allowRecurse = true
|
||||
}
|
||||
|
||||
// render
|
||||
|
Loading…
Reference in New Issue
Block a user