DebuggerEvent,
markRaw,
shallowReactive,
- readonly
+ readonly,
+ ReactiveEffectRunner
} from '../src/index'
import { ITERATE_KEY } from '../src/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)
--- /dev/null
+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
+}
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
}
class ComputedRefImpl<T> {
- public dep?: Set<ReactiveEffect> = undefined
+ public dep?: Dep = undefined
private _value!: T
private _dirty = true
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>()
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) {
}
}
+// 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
}
let dep = depsMap.get(key)
if (!dep) {
- depsMap.set(key, (dep = new Set()))
+ dep = createDep()
+ depsMap.set(key, dep)
}
const eventInfo = __DEV__
}
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) {
effects.push(...dep)
}
}
- triggerEffects(new Set(effects), eventInfo)
+ triggerEffects(createDep(effects), eventInfo)
}
}
-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
/**
* 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
}
if (isTracking()) {
ref = toRaw(ref)
if (!ref.dep) {
- ref.dep = new Set<ReactiveEffect>()
+ ref.dep = createDep()
}
if (__DEV__) {
trackEffects(ref.dep, {
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) {
}
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']
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) {
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