feat(reactivity): more efficient reactivity system (#5912)

fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464
This commit is contained in:
Johnson Chu 2023-10-27 22:25:09 +08:00 committed by 三咲智子 Kevin Deng
parent feb2f2edce
commit 16e06ca08f
No known key found for this signature in database
GPG Key ID: 69992F2250DFD93E
23 changed files with 811 additions and 543 deletions

View File

@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
// 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', () => {
@ -288,4 +288,167 @@ describe('reactivity/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)
})
})

View File

@ -1,57 +1,32 @@
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
})
@ -69,19 +44,18 @@ describe('deferred computed', () => {
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
})
@ -98,26 +72,24 @@ describe('deferred computed', () => {
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
})
@ -139,17 +111,17 @@ describe('deferred computed', () => {
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
})
@ -162,14 +134,13 @@ describe('deferred computed', () => {
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
})
@ -179,7 +150,6 @@ describe('deferred computed', () => {
c1.effect.stop()
// trigger
src.value++
await tick
expect(c1Spy).toHaveBeenCalledTimes(1)
})
})

View File

@ -1,5 +1,4 @@
import {
ref,
reactive,
effect,
stop,
@ -12,7 +11,8 @@ import {
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)', () => {
@ -574,8 +574,8 @@ describe('reactivity/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()
@ -821,26 +821,6 @@ describe('reactivity/effect', () => {
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(() => {}, {
@ -1015,4 +995,83 @@ describe('reactivity/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)
})
})
})

View File

@ -0,0 +1,81 @@
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)
})
})

View File

@ -99,6 +99,39 @@ describe('reactivity/reactive/Array', () => {
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)

View File

@ -2,7 +2,6 @@ import {
reactive,
readonly,
toRaw,
ReactiveFlags,
Target,
readonlyMap,
reactiveMap,
@ -11,14 +10,14 @@ import {
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,
@ -71,7 +70,9 @@ function createArrayInstrumentations() {
;(['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
}

View File

@ -1,6 +1,11 @@
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

View File

@ -1,8 +1,9 @@
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
@ -32,7 +33,6 @@ export class ComputedRefImpl<T> {
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _dirty = true
public _cacheable: boolean
constructor(
@ -42,10 +42,7 @@ export class ComputedRefImpl<T> {
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
@ -56,9 +53,10 @@ export class ComputedRefImpl<T> {
// 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
}
@ -66,6 +64,16 @@ export class ComputedRefImpl<T> {
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
}
/**

View File

@ -0,0 +1,30 @@
// 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
}

View File

@ -1,88 +1,6 @@
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

View File

@ -1,57 +1,17 @@
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
}
}

View File

@ -1,34 +1,8 @@
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
@ -47,13 +21,9 @@ export type DebuggerEventExtraInfo = {
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
@ -64,10 +34,6 @@ export class ReactiveEffect<T = any> {
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
@ -75,77 +41,115 @@ export class ReactiveEffect<T = any> {
// 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
}
}
@ -185,7 +189,11 @@ export function effect<T = any>(
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)
@ -208,6 +216,8 @@ export function stop(runner: ReactiveEffectRunner) {
}
export let shouldTrack = true
export let pauseScheduleStack = 0
const trackStack: boolean[] = []
/**
@ -234,196 +244,70 @@ export function resetTracking() {
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()))
}
export function pauseScheduling() {
pauseScheduleStack++
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
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!))
}
}
}
const queueEffectSchedulers: (() => void)[] = []
export function triggerEffects(
dep: Dep | ReactiveEffect[],
dep: Dep,
dirtyLevel: DirtyLevels,
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)
pauseScheduling()
for (const effect of dep.keys()) {
if (!effect.allowRecurse && effect._runnings) {
continue
}
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)
}
}
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
export function getDepFromReactive(object: any, key: string | number | symbol) {
return targetMap.get(object)?.get(key)
resetScheduling()
}

View File

@ -31,7 +31,6 @@ export {
shallowReadonly,
markRaw,
toRaw,
ReactiveFlags /* @remove */,
type Raw,
type DeepReadonly,
type ShallowReactive,
@ -49,12 +48,11 @@ export { deferredComputed } from './deferredComputed'
export {
effect,
stop,
trigger,
track,
enableTracking,
pauseTracking,
resetTracking,
ITERATE_KEY,
pauseScheduling,
resetScheduling,
ReactiveEffect,
type ReactiveEffectRunner,
type ReactiveEffectOptions,
@ -63,6 +61,7 @@ export {
type DebuggerEvent,
type DebuggerEventExtraInfo
} from './effect'
export { trigger, track, ITERATE_KEY } from './reactiveEffect'
export {
effectScope,
EffectScope,
@ -71,5 +70,6 @@ export {
} from './effectScope'
export {
TrackOpTypes /* @remove */,
TriggerOpTypes /* @remove */
} from './operations'
TriggerOpTypes /* @remove */,
ReactiveFlags /* @remove */
} from './constants'

View File

@ -1,15 +0,0 @@
// 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'
}

View File

@ -12,14 +12,7 @@ import {
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

View File

@ -0,0 +1,150 @@
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)
}

View File

@ -1,11 +1,10 @@
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,
@ -18,6 +17,8 @@ import {
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
@ -40,32 +41,44 @@ type RefBase<T> = {
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
)
}
}
@ -158,7 +171,7 @@ class RefImpl<T> {
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
triggerRefValue(this, DirtyLevels.Dirty, newVal)
}
}
}
@ -189,7 +202,7 @@ class RefImpl<T> {
* @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>

View File

@ -187,6 +187,7 @@ export function defineAsyncComponent<
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)
}
})

View File

@ -322,7 +322,7 @@ function doWatch(
? 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) {
@ -376,7 +376,7 @@ function doWatch(
scheduler = () => queueJob(job)
}
const effect = new ReactiveEffect(getter, scheduler)
const effect = new ReactiveEffect(getter, NOOP, scheduler)
const unwatch = () => {
effect.stop()

View File

@ -267,7 +267,12 @@ export const publicPropertiesMap: PublicPropertiesMap =
$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)

View File

@ -246,6 +246,7 @@ const BaseTransitionImpl: ComponentOptions = {
// #6835
// it also needs to be updated when active is undefined
if (instance.update.active !== false) {
instance.effect.dirty = true
instance.update()
}
}

View File

@ -93,6 +93,7 @@ function rerender(id: string, newRender?: Function) {
instance.renderCache = []
// this flag forces child components with slot content to update
isHmrUpdating = true
instance.effect.dirty = true
instance.update()
isHmrUpdating = false
})
@ -137,6 +138,7 @@ function reload(id: string, newComp: HMRComponent) {
// 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

View File

@ -1280,6 +1280,7 @@ function baseCreateRenderer(
// 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 {
@ -1544,11 +1545,16 @@ function baseCreateRenderer(
// 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