fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464
// mutate n
n.value++
// on the 2nd run, plusOne.value should have already updated.
- expect(plusOneValues).toMatchObject([1, 2, 2])
+ expect(plusOneValues).toMatchObject([1, 2])
})
it('should warn if trying to set a readonly computed', () => {
oldValue: 2
})
})
+
+ // https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
+ it('should query deps dirty sequentially', () => {
+ const cSpy = vi.fn()
+
+ const a = ref<null | { v: number }>({
+ v: 1
+ })
+ const b = computed(() => {
+ return a.value
+ })
+ const c = computed(() => {
+ cSpy()
+ return b.value?.v
+ })
+ const d = computed(() => {
+ if (b.value) {
+ return c.value
+ }
+ return 0
+ })
+
+ d.value
+ a.value!.v = 2
+ a.value = null
+ d.value
+ expect(cSpy).toHaveBeenCalledTimes(1)
+ })
+
+ // https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
+ it('chained computed dirty reallocation after querying dirty', () => {
+ let _msg: string | undefined
+
+ const items = ref<number[]>()
+ const isLoaded = computed(() => {
+ return !!items.value
+ })
+ const msg = computed(() => {
+ if (isLoaded.value) {
+ return 'The items are loaded'
+ } else {
+ return 'The items are not loaded'
+ }
+ })
+
+ effect(() => {
+ _msg = msg.value
+ })
+
+ items.value = [1, 2, 3]
+ items.value = [1, 2, 3]
+ items.value = undefined
+
+ expect(_msg).toBe('The items are not loaded')
+ })
+
+ it('chained computed dirty reallocation after trigger computed getter', () => {
+ let _msg: string | undefined
+
+ const items = ref<number[]>()
+ const isLoaded = computed(() => {
+ return !!items.value
+ })
+ const msg = computed(() => {
+ if (isLoaded.value) {
+ return 'The items are loaded'
+ } else {
+ return 'The items are not loaded'
+ }
+ })
+
+ _msg = msg.value
+ items.value = [1, 2, 3]
+ isLoaded.value // <- trigger computed getter
+ _msg = msg.value
+ items.value = undefined
+ _msg = msg.value
+
+ expect(_msg).toBe('The items are not loaded')
+ })
+
+ // https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
+ it('deps order should be consistent with the last time get value', () => {
+ const cSpy = vi.fn()
+
+ const a = ref(0)
+ const b = computed(() => {
+ return a.value % 3 !== 0
+ })
+ const c = computed(() => {
+ cSpy()
+ if (a.value % 3 === 2) {
+ return 'expensive'
+ }
+ return 'cheap'
+ })
+ const d = computed(() => {
+ return a.value % 3 === 2
+ })
+ const e = computed(() => {
+ if (b.value) {
+ if (d.value) {
+ return 'Avoiding expensive calculation'
+ }
+ }
+ return c.value
+ })
+
+ e.value
+ a.value++
+ e.value
+
+ expect(e.effect.deps.length).toBe(3)
+ expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
+ expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
+ expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
+ expect(cSpy).toHaveBeenCalledTimes(2)
+
+ a.value++
+ e.value
+
+ expect(cSpy).toHaveBeenCalledTimes(2)
+ })
+
+ it('should trigger by the second computed that maybe dirty', () => {
+ const cSpy = vi.fn()
+
+ const src1 = ref(0)
+ const src2 = ref(0)
+ const c1 = computed(() => src1.value)
+ const c2 = computed(() => (src1.value % 2) + src2.value)
+ const c3 = computed(() => {
+ cSpy()
+ c1.value
+ c2.value
+ })
+
+ c3.value
+ src1.value = 2
+ c3.value
+ expect(cSpy).toHaveBeenCalledTimes(2)
+ src2.value = 1
+ c3.value
+ expect(cSpy).toHaveBeenCalledTimes(3)
+ })
+
+ it('should trigger the second effect', () => {
+ const fnSpy = vi.fn()
+ const v = ref(1)
+ const c = computed(() => v.value)
+
+ effect(() => {
+ c.value
+ })
+ effect(() => {
+ c.value
+ fnSpy()
+ })
+
+ expect(fnSpy).toBeCalledTimes(1)
+ v.value = 2
+ expect(fnSpy).toBeCalledTimes(2)
+ })
})
-import { computed, deferredComputed, effect, ref } from '../src'
+import { computed, effect, ref } from '../src'
describe('deferred computed', () => {
- const tick = Promise.resolve()
-
- test('should only trigger once on multiple mutations', async () => {
+ test('should not trigger if value did not change', () => {
const src = ref(0)
- const c = deferredComputed(() => src.value)
+ const c = computed(() => src.value % 2)
const spy = vi.fn()
effect(() => {
spy(c.value)
})
expect(spy).toHaveBeenCalledTimes(1)
- src.value = 1
src.value = 2
- src.value = 3
- // not called yet
- expect(spy).toHaveBeenCalledTimes(1)
- await tick
- // should only trigger once
- expect(spy).toHaveBeenCalledTimes(2)
- expect(spy).toHaveBeenCalledWith(c.value)
- })
- test('should not trigger if value did not change', async () => {
- const src = ref(0)
- const c = deferredComputed(() => src.value % 2)
- const spy = vi.fn()
- effect(() => {
- spy(c.value)
- })
- expect(spy).toHaveBeenCalledTimes(1)
- src.value = 1
- src.value = 2
-
- await tick
// should not trigger
expect(spy).toHaveBeenCalledTimes(1)
src.value = 3
- src.value = 4
src.value = 5
- await tick
// should trigger because latest value changes
expect(spy).toHaveBeenCalledTimes(2)
})
- test('chained computed trigger', async () => {
+ test('chained computed trigger', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
expect(effectSpy).toHaveBeenCalledTimes(1)
src.value = 1
- await tick
expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c2Spy).toHaveBeenCalledTimes(2)
expect(effectSpy).toHaveBeenCalledTimes(2)
})
- test('chained computed avoid re-compute', async () => {
+ test('chained computed avoid re-compute', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
src.value = 2
src.value = 4
src.value = 6
- await tick
- // c1 should re-compute once.
- expect(c1Spy).toHaveBeenCalledTimes(2)
+ expect(c1Spy).toHaveBeenCalledTimes(4)
// c2 should not have to re-compute because c1 did not change.
expect(c2Spy).toHaveBeenCalledTimes(1)
// effect should not trigger because c2 did not change.
expect(effectSpy).toHaveBeenCalledTimes(1)
})
- test('chained computed value invalidation', async () => {
+ test('chained computed value invalidation', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
- const c2 = deferredComputed(() => {
+ const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
expect(c2Spy).toHaveBeenCalledTimes(2)
})
- test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
+ test('sync access of invalidated chained computed should not prevent final effect from running', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
- const c2 = deferredComputed(() => {
+ const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
src.value = 1
// sync access c2
c2.value
- await tick
expect(effectSpy).toHaveBeenCalledTimes(2)
})
- test('should not compute if deactivated before scheduler is called', async () => {
+ test('should not compute if deactivated before scheduler is called', () => {
const c1Spy = vi.fn()
const src = ref(0)
- const c1 = deferredComputed(() => {
+ const c1 = computed(() => {
c1Spy()
return src.value % 2
})
c1.effect.stop()
// trigger
src.value++
- await tick
expect(c1Spy).toHaveBeenCalledTimes(1)
})
})
import {
- ref,
reactive,
effect,
stop,
readonly,
ReactiveEffectRunner
} from '../src/index'
-import { ITERATE_KEY } from '../src/effect'
+import { pauseScheduling, resetScheduling } from '../src/effect'
+import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
expect(output.fx2).toBe(1 + 3 + 3)
expect(fx1Spy).toHaveBeenCalledTimes(1)
- // Invoked twice due to change of fx1.
- expect(fx2Spy).toHaveBeenCalledTimes(2)
+ // Invoked due to change of fx1.
+ expect(fx2Spy).toHaveBeenCalledTimes(1)
fx1Spy.mockClear()
fx2Spy.mockClear()
expect(dummy).toBe(3)
})
- // #5707
- // when an effect completes its run, it should clear the tracking bits of
- // its tracked deps. However, if the effect stops itself, the deps list is
- // emptied so their bits are never cleared.
- it('edge case: self-stopping effect tracking ref', () => {
- const c = ref(true)
- const runner = effect(() => {
- // reference ref
- if (!c.value) {
- // stop itself while running
- stop(runner)
- }
- })
- // trigger run
- c.value = !c.value
- // should clear bits
- expect((c as any).dep.w).toBe(0)
- expect((c as any).dep.n).toBe(0)
- })
-
it('events: onStop', () => {
const onStop = vi.fn()
const runner = effect(() => {}, {
expect(has).toBe(false)
})
})
+
+ it('should be triggered once with pauseScheduling', () => {
+ const counter = reactive({ num: 0 })
+
+ const counterSpy = vi.fn(() => counter.num)
+ effect(counterSpy)
+
+ counterSpy.mockClear()
+
+ pauseScheduling()
+ counter.num++
+ counter.num++
+ resetScheduling()
+ expect(counterSpy).toHaveBeenCalledTimes(1)
+ })
+
+ describe('empty dep cleanup', () => {
+ it('should remove the dep when the effect is stopped', () => {
+ const obj = reactive({ prop: 1 })
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ const runner = effect(() => obj.prop)
+ const dep = getDepFromReactive(toRaw(obj), 'prop')
+ expect(dep).toHaveLength(1)
+ obj.prop = 2
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(1)
+ stop(runner)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ obj.prop = 3
+ runner()
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ })
+
+ it('should only remove the dep when the last effect is stopped', () => {
+ const obj = reactive({ prop: 1 })
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ const runner1 = effect(() => obj.prop)
+ const dep = getDepFromReactive(toRaw(obj), 'prop')
+ expect(dep).toHaveLength(1)
+ const runner2 = effect(() => obj.prop)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(2)
+ obj.prop = 2
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(2)
+ stop(runner1)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(1)
+ obj.prop = 3
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
+ expect(dep).toHaveLength(1)
+ stop(runner2)
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ obj.prop = 4
+ runner1()
+ runner2()
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ })
+
+ it('should remove the dep when it is no longer used by the effect', () => {
+ const obj = reactive<{ a: number; b: number; c: 'a' | 'b' }>({
+ a: 1,
+ b: 2,
+ c: 'a'
+ })
+ expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+ effect(() => obj[obj.c])
+ const depC = getDepFromReactive(toRaw(obj), 'c')
+ expect(getDepFromReactive(toRaw(obj), 'a')).toHaveLength(1)
+ expect(getDepFromReactive(toRaw(obj), 'b')).toBeUndefined()
+ expect(depC).toHaveLength(1)
+ obj.c = 'b'
+ obj.a = 4
+ expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
+ expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
+ expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
+ expect(depC).toHaveLength(1)
+ })
+ })
})
--- /dev/null
+import {
+ ComputedRef,
+ computed,
+ effect,
+ reactive,
+ shallowRef as ref,
+ toRaw
+} from '../src/index'
+import { getDepFromReactive } from '../src/reactiveEffect'
+
+describe.skipIf(!global.gc)('reactivity/gc', () => {
+ const gc = () => {
+ return new Promise<void>(resolve => {
+ setTimeout(() => {
+ global.gc!()
+ resolve()
+ })
+ })
+ }
+
+ // #9233
+ it('should release computed cache', async () => {
+ const src = ref<{} | undefined>({})
+ const srcRef = new WeakRef(src.value!)
+
+ let c: ComputedRef | undefined = computed(() => src.value)
+
+ c.value // cache src value
+ src.value = undefined // release value
+ c = undefined // release computed
+
+ await gc()
+ expect(srcRef.deref()).toBeUndefined()
+ })
+
+ it('should release reactive property dep', async () => {
+ const src = reactive({ foo: 1 })
+
+ let c: ComputedRef | undefined = computed(() => src.foo)
+
+ c.value
+ expect(getDepFromReactive(toRaw(src), 'foo')).not.toBeUndefined()
+
+ c = undefined
+ await gc()
+ await gc()
+ expect(getDepFromReactive(toRaw(src), 'foo')).toBeUndefined()
+ })
+
+ it('should not release effect for ref', async () => {
+ const spy = vi.fn()
+ const src = ref(0)
+
+ effect(() => {
+ spy()
+ src.value
+ })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ await gc()
+ src.value++
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+
+ it('should not release effect for reactive', async () => {
+ const spy = vi.fn()
+ const src = reactive({ foo: 1 })
+
+ effect(() => {
+ spy()
+ src.foo
+ })
+
+ expect(spy).toHaveBeenCalledTimes(1)
+
+ await gc()
+ src.foo++
+ expect(spy).toHaveBeenCalledTimes(2)
+ })
+})
expect(fn).toHaveBeenCalledTimes(1)
})
+ test('shift on Array should trigger dependency once', () => {
+ const arr = reactive([1, 2, 3])
+ const fn = vi.fn()
+ effect(() => {
+ for (let i = 0; i < arr.length; i++) {
+ arr[i]
+ }
+ fn()
+ })
+ expect(fn).toHaveBeenCalledTimes(1)
+ arr.shift()
+ expect(fn).toHaveBeenCalledTimes(2)
+ })
+
+ //#6018
+ test('edge case: avoid trigger effect in deleteProperty when array length-decrease mutation methods called', () => {
+ const arr = ref([1])
+ const fn1 = vi.fn()
+ const fn2 = vi.fn()
+ effect(() => {
+ fn1()
+ if (arr.value.length > 0) {
+ arr.value.slice()
+ fn2()
+ }
+ })
+ expect(fn1).toHaveBeenCalledTimes(1)
+ expect(fn2).toHaveBeenCalledTimes(1)
+ arr.value.splice(0)
+ expect(fn1).toHaveBeenCalledTimes(2)
+ expect(fn2).toHaveBeenCalledTimes(1)
+ })
+
test('add existing index on Array should not trigger length dependency', () => {
const array = new Array(3)
const observed = reactive(array)
reactive,
readonly,
toRaw,
- ReactiveFlags,
Target,
readonlyMap,
reactiveMap,
isReadonly,
isShallow
} from './reactive'
-import { TrackOpTypes, TriggerOpTypes } from './operations'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import {
- track,
- trigger,
- ITERATE_KEY,
pauseTracking,
- resetTracking
+ resetTracking,
+ pauseScheduling,
+ resetScheduling
} from './effect'
+import { track, trigger, ITERATE_KEY } from './reactiveEffect'
import {
isObject,
hasOwn,
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
+ pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
+ resetScheduling()
resetTracking()
return res
}
-import { toRaw, ReactiveFlags, toReactive, toReadonly } from './reactive'
-import { track, trigger, ITERATE_KEY, MAP_KEY_ITERATE_KEY } from './effect'
-import { TrackOpTypes, TriggerOpTypes } from './operations'
+import { toRaw, toReactive, toReadonly } from './reactive'
+import {
+ track,
+ trigger,
+ ITERATE_KEY,
+ MAP_KEY_ITERATE_KEY
+} from './reactiveEffect'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { capitalize, hasOwn, hasChanged, toRawType, isMap } from '@vue/shared'
export type CollectionTypes = IterableCollections | WeakCollections
import { DebuggerOptions, ReactiveEffect } from './effect'
import { Ref, trackRefValue, triggerRefValue } from './ref'
-import { isFunction, NOOP } from '@vue/shared'
-import { ReactiveFlags, toRaw } from './reactive'
+import { hasChanged, isFunction, NOOP } from '@vue/shared'
+import { toRaw } from './reactive'
import { Dep } from './dep'
+import { DirtyLevels, ReactiveFlags } from './constants'
declare const ComputedRefSymbol: unique symbol
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
- public _dirty = true
public _cacheable: boolean
constructor(
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
- if (!this._dirty) {
- this._dirty = true
- triggerRefValue(this)
- }
+ triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty)
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
- if (self._dirty || !self._cacheable) {
- self._dirty = false
- self._value = self.effect.run()!
+ if (!self._cacheable || self.effect.dirty) {
+ if (hasChanged(self._value, (self._value = self.effect.run()!))) {
+ triggerRefValue(self, DirtyLevels.ComputedValueDirty)
+ }
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
+
+ // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
+ get _dirty() {
+ return this.effect.dirty
+ }
+
+ set _dirty(v) {
+ this.effect.dirty = v
+ }
+ // #endregion
}
/**
--- /dev/null
+// using literal strings instead of numbers so that it's easier to inspect
+// debugger events
+
+export const enum TrackOpTypes {
+ GET = 'get',
+ HAS = 'has',
+ ITERATE = 'iterate'
+}
+
+export const enum TriggerOpTypes {
+ SET = 'set',
+ ADD = 'add',
+ DELETE = 'delete',
+ CLEAR = 'clear'
+}
+
+export const enum ReactiveFlags {
+ SKIP = '__v_skip',
+ IS_REACTIVE = '__v_isReactive',
+ IS_READONLY = '__v_isReadonly',
+ IS_SHALLOW = '__v_isShallow',
+ RAW = '__v_raw'
+}
+
+export const enum DirtyLevels {
+ NotDirty = 0,
+ ComputedValueMaybeDirty = 1,
+ ComputedValueDirty = 2,
+ Dirty = 3
+}
-import { Dep } from './dep'
-import { ReactiveEffect } from './effect'
-import { ComputedGetter, ComputedRef } from './computed'
-import { ReactiveFlags, toRaw } from './reactive'
-import { trackRefValue, triggerRefValue } from './ref'
+import { computed } from './computed'
-const tick = /*#__PURE__*/ Promise.resolve()
-const queue: any[] = []
-let queued = false
-
-const scheduler = (fn: any) => {
- queue.push(fn)
- if (!queued) {
- queued = true
- tick.then(flush)
- }
-}
-
-const flush = () => {
- for (let i = 0; i < queue.length; i++) {
- queue[i]()
- }
- queue.length = 0
- queued = false
-}
-
-class DeferredComputedRefImpl<T> {
- public dep?: Dep = undefined
-
- private _value!: T
- private _dirty = true
- public readonly effect: ReactiveEffect<T>
-
- public readonly __v_isRef = true
- public readonly [ReactiveFlags.IS_READONLY] = true
-
- constructor(getter: ComputedGetter<T>) {
- let compareTarget: any
- let hasCompareTarget = false
- let scheduled = false
- this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
- if (this.dep) {
- if (computedTrigger) {
- compareTarget = this._value
- hasCompareTarget = true
- } else if (!scheduled) {
- const valueToCompare = hasCompareTarget ? compareTarget : this._value
- scheduled = true
- hasCompareTarget = false
- scheduler(() => {
- if (this.effect.active && this._get() !== valueToCompare) {
- triggerRefValue(this)
- }
- scheduled = false
- })
- }
- // chained upstream computeds are notified synchronously to ensure
- // value invalidation in case of sync access; normal effects are
- // deferred to be triggered in scheduler.
- for (const e of this.dep) {
- if (e.computed instanceof DeferredComputedRefImpl) {
- e.scheduler!(true /* computedTrigger */)
- }
- }
- }
- this._dirty = true
- })
- this.effect.computed = this as any
- }
-
- private _get() {
- if (this._dirty) {
- this._dirty = false
- return (this._value = this.effect.run()!)
- }
- return this._value
- }
-
- get value() {
- trackRefValue(this)
- // the computed ref may get wrapped by other proxies e.g. readonly() #3376
- return toRaw(this)._get()
- }
-}
-
-export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
- return new DeferredComputedRefImpl(getter) as any
-}
+/**
+ * @deprecated use `computed` instead. See #5912
+ */
+export const deferredComputed = computed
-import { ReactiveEffect, trackOpBit } from './effect'
+import type { ReactiveEffect } from './effect'
+import type { ComputedRefImpl } from './computed'
-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 whether the dependency
- * was/is tracked.
- */
-type TrackedMarkers = {
- /**
- * wasTracked
- */
- w: number
- /**
- * newTracked
- */
- n: number
+export type Dep = Map<ReactiveEffect, number> & {
+ cleanup: () => void
+ computed?: ComputedRefImpl<any>
}
-export const createDep = (effects?: ReactiveEffect[]): Dep => {
- const dep = new Set<ReactiveEffect>(effects) as Dep
- dep.w = 0
- dep.n = 0
+export const createDep = (
+ cleanup: () => void,
+ computed?: ComputedRefImpl<any>
+): Dep => {
+ const dep = new Map() as Dep
+ dep.cleanup = cleanup
+ dep.computed = computed
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 // set was tracked
- }
- }
-}
-
-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
- }
- // clear bits
- dep.w &= ~trackOpBit
- dep.n &= ~trackOpBit
- }
- deps.length = ptr
- }
-}
-import { TrackOpTypes, TriggerOpTypes } from './operations'
-import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
+import { NOOP, extend } from '@vue/shared'
+import type { ComputedRefImpl } from './computed'
+import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
+import type { Dep } from './dep'
import { EffectScope, recordEffectScope } from './effectScope'
-import {
- createDep,
- Dep,
- finalizeDepMarkers,
- initDepMarkers,
- newTracked,
- wasTracked
-} from './dep'
-import { ComputedRefImpl } from './computed'
-
-// 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 KeyToDepMap = Map<any, Dep>
-const targetMap = new WeakMap<object, KeyToDepMap>()
-
-// The number of effects currently being tracked recursively.
-let effectTrackDepth = 0
-
-export let trackOpBit = 1
-
-/**
- * The bitwise track markers support at most 30 levels of 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 type EffectScheduler = (...args: any[]) => any
export let activeEffect: ReactiveEffect | undefined
-export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
-export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
-
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
- parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
allowRecurse?: boolean
- /**
- * @internal
- */
- private deferStop?: boolean
onStop?: () => void
// dev only
// dev only
onTrigger?: (event: DebuggerEvent) => void
+ /**
+ * @internal
+ */
+ _dirtyLevel = DirtyLevels.Dirty
+ /**
+ * @internal
+ */
+ _trackId = 0
+ /**
+ * @internal
+ */
+ _runnings = 0
+ /**
+ * @internal
+ */
+ _queryings = 0
+ /**
+ * @internal
+ */
+ _depsLength = 0
+
constructor(
public fn: () => T,
- public scheduler: EffectScheduler | null = null,
+ public trigger: () => void,
+ public scheduler?: EffectScheduler,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
+ public get dirty() {
+ if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
+ this._dirtyLevel = DirtyLevels.NotDirty
+ this._queryings++
+ pauseTracking()
+ for (const dep of this.deps) {
+ if (dep.computed) {
+ triggerComputed(dep.computed)
+ if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
+ break
+ }
+ }
+ }
+ resetTracking()
+ this._queryings--
+ }
+ return this._dirtyLevel >= DirtyLevels.ComputedValueDirty
+ }
+
+ public set dirty(v) {
+ this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
+ }
+
run() {
+ this._dirtyLevel = DirtyLevels.NotDirty
if (!this.active) {
return this.fn()
}
- let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
- while (parent) {
- if (parent === this) {
- return
- }
- parent = parent.parent
- }
+ let lastEffect = activeEffect
try {
- this.parent = activeEffect
- activeEffect = this
shouldTrack = true
-
- trackOpBit = 1 << ++effectTrackDepth
-
- if (effectTrackDepth <= maxMarkerBits) {
- initDepMarkers(this)
- } else {
- cleanupEffect(this)
- }
+ activeEffect = this
+ this._runnings++
+ preCleanupEffect(this)
return this.fn()
} finally {
- if (effectTrackDepth <= maxMarkerBits) {
- finalizeDepMarkers(this)
- }
-
- trackOpBit = 1 << --effectTrackDepth
-
- activeEffect = this.parent
+ postCleanupEffect(this)
+ this._runnings--
+ activeEffect = lastEffect
shouldTrack = lastShouldTrack
- this.parent = undefined
-
- if (this.deferStop) {
- this.stop()
- }
}
}
stop() {
- // stopped while running itself - defer the cleanup
- if (activeEffect === this) {
- this.deferStop = true
- } else if (this.active) {
- cleanupEffect(this)
- if (this.onStop) {
- this.onStop()
- }
+ if (this.active) {
+ preCleanupEffect(this)
+ postCleanupEffect(this)
+ this.onStop?.()
this.active = false
}
}
}
-function cleanupEffect(effect: ReactiveEffect) {
- const { deps } = effect
- if (deps.length) {
- for (let i = 0; i < deps.length; i++) {
- deps[i].delete(effect)
+function triggerComputed(computed: ComputedRefImpl<any>) {
+ return computed.value
+}
+
+function preCleanupEffect(effect: ReactiveEffect) {
+ effect._trackId++
+ effect._depsLength = 0
+}
+
+function postCleanupEffect(effect: ReactiveEffect) {
+ if (effect.deps && effect.deps.length > effect._depsLength) {
+ for (let i = effect._depsLength; i < effect.deps.length; i++) {
+ cleanupDepEffect(effect.deps[i], effect)
+ }
+ effect.deps.length = effect._depsLength
+ }
+}
+
+function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
+ const trackId = dep.get(effect)
+ if (trackId !== undefined && effect._trackId !== trackId) {
+ dep.delete(effect)
+ if (dep.size === 0) {
+ dep.cleanup()
}
- deps.length = 0
}
}
fn = (fn as ReactiveEffectRunner).effect.fn
}
- const _effect = new ReactiveEffect(fn)
+ const _effect = new ReactiveEffect(fn, NOOP, () => {
+ if (_effect.dirty) {
+ _effect.run()
+ }
+ })
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
export let shouldTrack = true
+export let pauseScheduleStack = 0
+
const trackStack: boolean[] = []
/**
shouldTrack = last === undefined ? true : last
}
-/**
- * Tracks access to a reactive property.
- *
- * This will check which effect is running at the moment and record it as dep
- * which records all effects that depend on the reactive property.
- *
- * @param target - Object holding the reactive property.
- * @param type - Defines the type of access to the reactive property.
- * @param key - Identifier of the reactive property to track.
- */
-export function track(target: object, type: TrackOpTypes, key: unknown) {
- if (shouldTrack && activeEffect) {
- let depsMap = targetMap.get(target)
- if (!depsMap) {
- targetMap.set(target, (depsMap = new Map()))
- }
- let dep = depsMap.get(key)
- if (!dep) {
- depsMap.set(key, (dep = createDep()))
- }
-
- const eventInfo = __DEV__
- ? { effect: activeEffect, target, type, key }
- : undefined
+export function pauseScheduling() {
+ pauseScheduleStack++
+}
- trackEffects(dep, eventInfo)
+export function resetScheduling() {
+ pauseScheduleStack--
+ while (!pauseScheduleStack && queueEffectSchedulers.length) {
+ queueEffectSchedulers.shift()!()
}
}
-export function trackEffects(
+export function trackEffect(
+ effect: ReactiveEffect,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
- let shouldTrack = false
- if (effectTrackDepth <= maxMarkerBits) {
- if (!newTracked(dep)) {
- dep.n |= trackOpBit // set newly tracked
- shouldTrack = !wasTracked(dep)
- }
- } else {
- // Full cleanup mode.
- shouldTrack = !dep.has(activeEffect!)
- }
-
- if (shouldTrack) {
- dep.add(activeEffect!)
- activeEffect!.deps.push(dep)
- if (__DEV__ && activeEffect!.onTrack) {
- activeEffect!.onTrack(
- extend(
- {
- effect: activeEffect!
- },
- debuggerEventExtraInfo!
- )
- )
- }
- }
-}
-
-/**
- * Finds all deps associated with the target (or a specific property) and
- * triggers the effects stored within.
- *
- * @param target - The reactive object.
- * @param type - Defines the type of the operation that needs to trigger effects.
- * @param key - Can be used to target a specific reactive property in the target object.
- */
-export function trigger(
- target: object,
- type: TriggerOpTypes,
- key?: unknown,
- newValue?: unknown,
- oldValue?: unknown,
- oldTarget?: Map<unknown, unknown> | Set<unknown>
-) {
- const depsMap = targetMap.get(target)
- if (!depsMap) {
- // never been tracked
- return
- }
-
- let deps: (Dep | undefined)[] = []
- if (type === TriggerOpTypes.CLEAR) {
- // collection being cleared
- // trigger all effects for target
- deps = [...depsMap.values()]
- } else if (key === 'length' && isArray(target)) {
- const newLength = Number(newValue)
- depsMap.forEach((dep, key) => {
- if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
- deps.push(dep)
- }
- })
- } else {
- // schedule runs for SET | ADD | DELETE
- if (key !== void 0) {
- deps.push(depsMap.get(key))
- }
-
- // also run for iteration key on ADD | DELETE | Map.SET
- switch (type) {
- case TriggerOpTypes.ADD:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- } else if (isIntegerKey(key)) {
- // new index added to array -> length changes
- deps.push(depsMap.get('length'))
- }
- break
- case TriggerOpTypes.DELETE:
- if (!isArray(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- if (isMap(target)) {
- deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
- }
- }
- break
- case TriggerOpTypes.SET:
- if (isMap(target)) {
- deps.push(depsMap.get(ITERATE_KEY))
- }
- break
- }
- }
-
- const eventInfo = __DEV__
- ? { target, type, key, newValue, oldValue, oldTarget }
- : undefined
-
- if (deps.length === 1) {
- if (deps[0]) {
- if (__DEV__) {
- triggerEffects(deps[0], eventInfo)
- } else {
- triggerEffects(deps[0])
- }
- }
- } else {
- const effects: ReactiveEffect[] = []
- for (const dep of deps) {
- if (dep) {
- effects.push(...dep)
+ if (dep.get(effect) !== effect._trackId) {
+ dep.set(effect, effect._trackId)
+ const oldDep = effect.deps[effect._depsLength]
+ if (oldDep !== dep) {
+ if (oldDep) {
+ cleanupDepEffect(oldDep, effect)
}
+ effect.deps[effect._depsLength++] = dep
+ } else {
+ effect._depsLength++
}
if (__DEV__) {
- triggerEffects(createDep(effects), eventInfo)
- } else {
- triggerEffects(createDep(effects))
+ effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
}
}
}
-export function triggerEffects(
- dep: Dep | ReactiveEffect[],
- debuggerEventExtraInfo?: DebuggerEventExtraInfo
-) {
- // spread into array for stabilization
- const effects = isArray(dep) ? dep : [...dep]
- for (const effect of effects) {
- if (effect.computed) {
- triggerEffect(effect, debuggerEventExtraInfo)
- }
- }
- for (const effect of effects) {
- if (!effect.computed) {
- triggerEffect(effect, debuggerEventExtraInfo)
- }
- }
-}
+const queueEffectSchedulers: (() => void)[] = []
-function triggerEffect(
- effect: ReactiveEffect,
+export function triggerEffects(
+ dep: Dep,
+ dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
- if (effect !== activeEffect || effect.allowRecurse) {
- if (__DEV__ && effect.onTrigger) {
- effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
+ pauseScheduling()
+ for (const effect of dep.keys()) {
+ if (!effect.allowRecurse && effect._runnings) {
+ continue
}
- if (effect.scheduler) {
- effect.scheduler()
- } else {
- effect.run()
+ if (
+ effect._dirtyLevel < dirtyLevel &&
+ (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
+ ) {
+ const lastDirtyLevel = effect._dirtyLevel
+ effect._dirtyLevel = dirtyLevel
+ if (
+ lastDirtyLevel === DirtyLevels.NotDirty &&
+ (!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
+ ) {
+ if (__DEV__) {
+ effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
+ }
+ effect.trigger()
+ if (effect.scheduler) {
+ queueEffectSchedulers.push(effect.scheduler)
+ }
+ }
}
}
-}
-
-export function getDepFromReactive(object: any, key: string | number | symbol) {
- return targetMap.get(object)?.get(key)
+ resetScheduling()
}
shallowReadonly,
markRaw,
toRaw,
- ReactiveFlags /* @remove */,
type Raw,
type DeepReadonly,
type ShallowReactive,
export {
effect,
stop,
- trigger,
- track,
enableTracking,
pauseTracking,
resetTracking,
- ITERATE_KEY,
+ pauseScheduling,
+ resetScheduling,
ReactiveEffect,
type ReactiveEffectRunner,
type ReactiveEffectOptions,
type DebuggerEvent,
type DebuggerEventExtraInfo
} from './effect'
+export { trigger, track, ITERATE_KEY } from './reactiveEffect'
export {
effectScope,
EffectScope,
} from './effectScope'
export {
TrackOpTypes /* @remove */,
- TriggerOpTypes /* @remove */
-} from './operations'
+ TriggerOpTypes /* @remove */,
+ ReactiveFlags /* @remove */
+} from './constants'
+++ /dev/null
-// using literal strings instead of numbers so that it's easier to inspect
-// debugger events
-
-export const enum TrackOpTypes {
- GET = 'get',
- HAS = 'has',
- ITERATE = 'iterate'
-}
-
-export const enum TriggerOpTypes {
- SET = 'set',
- ADD = 'add',
- DELETE = 'delete',
- CLEAR = 'clear'
-}
shallowReadonlyCollectionHandlers
} from './collectionHandlers'
import type { UnwrapRefSimple, Ref, RawSymbol } from './ref'
-
-export const enum ReactiveFlags {
- SKIP = '__v_skip',
- IS_REACTIVE = '__v_isReactive',
- IS_READONLY = '__v_isReadonly',
- IS_SHALLOW = '__v_isShallow',
- RAW = '__v_raw'
-}
+import { ReactiveFlags } from './constants'
export interface Target {
[ReactiveFlags.SKIP]?: boolean
--- /dev/null
+import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
+import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
+import { createDep, Dep } from './dep'
+import {
+ activeEffect,
+ pauseScheduling,
+ resetScheduling,
+ shouldTrack,
+ trackEffect,
+ triggerEffects
+} from './effect'
+
+// 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 KeyToDepMap = Map<any, Dep>
+const targetMap = new WeakMap<object, KeyToDepMap>()
+
+export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
+export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
+
+/**
+ * Tracks access to a reactive property.
+ *
+ * This will check which effect is running at the moment and record it as dep
+ * which records all effects that depend on the reactive property.
+ *
+ * @param target - Object holding the reactive property.
+ * @param type - Defines the type of access to the reactive property.
+ * @param key - Identifier of the reactive property to track.
+ */
+export function track(target: object, type: TrackOpTypes, key: unknown) {
+ if (shouldTrack && activeEffect) {
+ let depsMap = targetMap.get(target)
+ if (!depsMap) {
+ targetMap.set(target, (depsMap = new Map()))
+ }
+ let dep = depsMap.get(key)
+ if (!dep) {
+ depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
+ }
+ trackEffect(
+ activeEffect,
+ dep,
+ __DEV__
+ ? {
+ target,
+ type,
+ key
+ }
+ : void 0
+ )
+ }
+}
+
+/**
+ * Finds all deps associated with the target (or a specific property) and
+ * triggers the effects stored within.
+ *
+ * @param target - The reactive object.
+ * @param type - Defines the type of the operation that needs to trigger effects.
+ * @param key - Can be used to target a specific reactive property in the target object.
+ */
+export function trigger(
+ target: object,
+ type: TriggerOpTypes,
+ key?: unknown,
+ newValue?: unknown,
+ oldValue?: unknown,
+ oldTarget?: Map<unknown, unknown> | Set<unknown>
+) {
+ const depsMap = targetMap.get(target)
+ if (!depsMap) {
+ // never been tracked
+ return
+ }
+
+ let deps: (Dep | undefined)[] = []
+ if (type === TriggerOpTypes.CLEAR) {
+ // collection being cleared
+ // trigger all effects for target
+ deps = [...depsMap.values()]
+ } else if (key === 'length' && isArray(target)) {
+ const newLength = Number(newValue)
+ depsMap.forEach((dep, key) => {
+ if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
+ deps.push(dep)
+ }
+ })
+ } else {
+ // schedule runs for SET | ADD | DELETE
+ if (key !== void 0) {
+ deps.push(depsMap.get(key))
+ }
+
+ // also run for iteration key on ADD | DELETE | Map.SET
+ switch (type) {
+ case TriggerOpTypes.ADD:
+ if (!isArray(target)) {
+ deps.push(depsMap.get(ITERATE_KEY))
+ if (isMap(target)) {
+ deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ }
+ } else if (isIntegerKey(key)) {
+ // new index added to array -> length changes
+ deps.push(depsMap.get('length'))
+ }
+ break
+ case TriggerOpTypes.DELETE:
+ if (!isArray(target)) {
+ deps.push(depsMap.get(ITERATE_KEY))
+ if (isMap(target)) {
+ deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
+ }
+ }
+ break
+ case TriggerOpTypes.SET:
+ if (isMap(target)) {
+ deps.push(depsMap.get(ITERATE_KEY))
+ }
+ break
+ }
+ }
+
+ pauseScheduling()
+ for (const dep of deps) {
+ if (dep) {
+ triggerEffects(
+ dep,
+ DirtyLevels.Dirty,
+ __DEV__
+ ? {
+ target,
+ type,
+ key,
+ newValue,
+ oldValue,
+ oldTarget
+ }
+ : void 0
+ )
+ }
+ }
+ resetScheduling()
+}
+
+export function getDepFromReactive(object: any, key: string | number | symbol) {
+ return targetMap.get(object)?.get(key)
+}
import {
activeEffect,
- getDepFromReactive,
shouldTrack,
- trackEffects,
+ trackEffect,
triggerEffects
} from './effect'
-import { TrackOpTypes, TriggerOpTypes } from './operations'
+import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
import {
isProxy,
import type { ShallowReactiveMarker } from './reactive'
import { CollectionTypes } from './collectionHandlers'
import { createDep, Dep } from './dep'
+import { ComputedRefImpl } from './computed'
+import { getDepFromReactive } from './reactiveEffect'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
- if (__DEV__) {
- trackEffects(ref.dep || (ref.dep = createDep()), {
- target: ref,
- type: TrackOpTypes.GET,
- key: 'value'
- })
- } else {
- trackEffects(ref.dep || (ref.dep = createDep()))
- }
+ trackEffect(
+ activeEffect,
+ ref.dep ||
+ (ref.dep = createDep(
+ () => (ref.dep = undefined),
+ ref instanceof ComputedRefImpl ? ref : undefined
+ )),
+ __DEV__
+ ? {
+ target: ref,
+ type: TrackOpTypes.GET,
+ key: 'value'
+ }
+ : void 0
+ )
}
}
-export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
+export function triggerRefValue(
+ ref: RefBase<any>,
+ dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
+ newVal?: any
+) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
- if (__DEV__) {
- triggerEffects(dep, {
- target: ref,
- type: TriggerOpTypes.SET,
- key: 'value',
- newValue: newVal
- })
- } else {
- triggerEffects(dep)
- }
+ triggerEffects(
+ dep,
+ dirtyLevel,
+ __DEV__
+ ? {
+ target: ref,
+ type: TriggerOpTypes.SET,
+ key: 'value',
+ newValue: newVal
+ }
+ : void 0
+ )
}
}
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
- triggerRefValue(this, newVal)
+ triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref) {
- triggerRefValue(ref, __DEV__ ? ref.value : void 0)
+ triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
}
export type MaybeRef<T = any> = T | Ref<T>
if (instance.parent && isKeepAlive(instance.parent.vnode)) {
// parent is keep-alive, force update so the loaded component's
// name is taken into account
+ instance.parent.effect.dirty = true
queueJob(instance.parent.update)
}
})
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
- if (!effect.active) {
+ if (!effect.active || !effect.dirty) {
return
}
if (cb) {
scheduler = () => queueJob(job)
}
- const effect = new ReactiveEffect(getter, scheduler)
+ const effect = new ReactiveEffect(getter, NOOP, scheduler)
const unwatch = () => {
effect.stop()
$root: i => getPublicInstance(i.root),
$emit: i => i.emit,
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
- $forceUpdate: i => i.f || (i.f = () => queueJob(i.update)),
+ $forceUpdate: i =>
+ i.f ||
+ (i.f = () => {
+ i.effect.dirty = true
+ queueJob(i.update)
+ }),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP)
} as PublicPropertiesMap)
// #6835
// it also needs to be updated when active is undefined
if (instance.update.active !== false) {
+ instance.effect.dirty = true
instance.update()
}
}
instance.renderCache = []
// this flag forces child components with slot content to update
isHmrUpdating = true
+ instance.effect.dirty = true
instance.update()
isHmrUpdating = false
})
// 4. Force the parent instance to re-render. This will cause all updated
// components to be unmounted and re-mounted. Queue the update so that we
// don't end up forcing the same parent to re-render multiple times.
+ instance.parent.effect.dirty = true
queueJob(instance.parent.update)
} else if (instance.appContext.reload) {
// root instance mounted via createApp() has a reload method
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect.
+ instance.effect.dirty = true
instance.update()
}
} else {
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
+ NOOP,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
- const update: SchedulerJob = (instance.update = () => effect.run())
+ const update: SchedulerJob = (instance.update = () => {
+ if (effect.dirty) {
+ effect.run()
+ }
+ })
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates