]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(reactivity): more efficient reactivity system (#5912)
authorJohnson Chu <johnsoncodehk@gmail.com>
Fri, 27 Oct 2023 14:25:09 +0000 (22:25 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Fri, 27 Oct 2023 14:28:40 +0000 (23:28 +0900)
fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464

23 files changed:
packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/__tests__/deferredComputed.spec.ts
packages/reactivity/__tests__/effect.spec.ts
packages/reactivity/__tests__/gc.spec.ts [new file with mode: 0644]
packages/reactivity/__tests__/reactiveArray.spec.ts
packages/reactivity/src/baseHandlers.ts
packages/reactivity/src/collectionHandlers.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/constants.ts [new file with mode: 0644]
packages/reactivity/src/deferredComputed.ts
packages/reactivity/src/dep.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/index.ts
packages/reactivity/src/operations.ts [deleted file]
packages/reactivity/src/reactive.ts
packages/reactivity/src/reactiveEffect.ts [new file with mode: 0644]
packages/reactivity/src/ref.ts
packages/runtime-core/src/apiAsyncComponent.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/renderer.ts

index c044b5feb35f5f7ef016a8c640bbce855194ea9b..d9b8f888cafea0ee3311172a8ef08132b1c263b0 100644 (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)
+  })
 })
index 100f14ae3582709950d01a46a59b0b367671ccdf..8e78ba959c3ecd5b95ceb1ccc98411c9e9f997b5 100644 (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)
   })
 })
index e34c7b31e40ad4d59eb1e3b6c817baed65883c50..2ebb2edea8a7dde2106876ef363366a4c32d048b 100644 (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)
+    })
+  })
 })
diff --git a/packages/reactivity/__tests__/gc.spec.ts b/packages/reactivity/__tests__/gc.spec.ts
new file mode 100644 (file)
index 0000000..7676a0e
--- /dev/null
@@ -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)
+  })
+})
index 808c5aa5529b717029845f076a03b433dd862dab..f4eb7b5838452482fe16f9bd119da6c0565fea80 100644 (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)
index 259b44a1edcc17cc6ae04403c91a628a168bbfbe..36e4d311b4b9a91bf080a369d2fc7c241d07f7e3 100644 (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
     }
index 1d07af3be8c7e3d00447ee82abb1ef6f0667da80..e8d99840f716effd6b4c0018686251d9c2332026 100644 (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
index b24484c9e62cb8ba8029f4514c1725130acbda4e..09247360d06e6b759cd7c7ba99972083a535f19d 100644 (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
 }
 
 /**
diff --git a/packages/reactivity/src/constants.ts b/packages/reactivity/src/constants.ts
new file mode 100644 (file)
index 0000000..4ad2ec3
--- /dev/null
@@ -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
+}
index a23122046a4bf8010c8d56aa41aeffd85a71108a..1dbba1f3f03bac36c67e4f8371d48548fbf9ae19 100644 (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
index 8677f575756bcd16a6a2d4887591320d00452809..eafb2a8af3f59cd949d95fa48e0e841788f4740b 100644 (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
-  }
-}
index c982dbd0b5a96d861b58c19de71259cb96030b22..3a25295011c05b95187d8fa5da5e43ceff4598fb 100644 (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()))
-    }
-
-    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()
 }
index ee4da5b19357a187504adc7cd47d49cbfedb1446..9497527e81e3aaafe1fc9c98a90795829b005e0b 100644 (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'
diff --git a/packages/reactivity/src/operations.ts b/packages/reactivity/src/operations.ts
deleted file mode 100644 (file)
index 1b96e98..0000000
+++ /dev/null
@@ -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'
-}
index 1881955cf1ce1533cf2dec9b69886e45843b6c3b..2904c69abe29c775343325a082d54e11708fb988 100644 (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
diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts
new file mode 100644 (file)
index 0000000..d3474db
--- /dev/null
@@ -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)
+}
index 915f5760878ebf0c6fef53d2f59ad79d71ad5850..5a4dd710eab8a57f095d2b4ac16235d19610bec7 100644 (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>
index 342339042ef0b25a9ef45eeb53140c99c46242b4..535cb83fb5deec904b0c3c46dd7d9ba6057af8cd 100644 (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)
           }
         })
index c307c4198a3028c925d72672d957725fdece64b8..cedebb01af61c94e934bbba0a79b237be58bdc7a 100644 (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()
index b7ef1e07302d6c55a49ab6adba55302d27515992..7b552c8f92ac1de0a881f29ffa0d85e9ff084211 100644 (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)
index 9cb80b94ef0146c4fd806a6dc91c8e424cc299a2..ef0632384d6747f507440ff84dd52addcddab65a 100644 (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()
             }
           }
index 1ce66a3da1ec69c194f7d19051f3e2b797317771..cdf291989bda29f0614457c6f87ada8b1f099dc2 100644 (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
index 8799ecd473ca8ddc167970ed7acee7347c10ffce..8dbc1c796d52fbb7e5b3e6e2ec72d13e4530caf8 100644 (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