]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
Refactor reactivity system to use version counting and doubly-linked list tracking...
authorEvan You <yyx990803@gmail.com>
Sun, 25 Feb 2024 08:51:49 +0000 (16:51 +0800)
committerGitHub <noreply@github.com>
Sun, 25 Feb 2024 08:51:49 +0000 (16:51 +0800)
Bug fixes
close #10236
close #10069

PRs made stale by this one
close #10290
close #10354
close #10189
close #9480

38 files changed:
packages/reactivity/__benchmarks__/computed.bench.ts [new file with mode: 0644]
packages/reactivity/__benchmarks__/effect.bench.ts [new file with mode: 0644]
packages/reactivity/__benchmarks__/reactiveArray.bench.ts [moved from packages/reactivity/__tests__/reactiveArray.bench.ts with 94% similarity]
packages/reactivity/__benchmarks__/reactiveMap.bench.ts [moved from packages/reactivity/__tests__/reactiveMap.bench.ts with 97% similarity]
packages/reactivity/__benchmarks__/reactiveObject.bench.ts [new file with mode: 0644]
packages/reactivity/__benchmarks__/ref.bench.ts [moved from packages/reactivity/__tests__/ref.bench.ts with 99% similarity]
packages/reactivity/__tests__/computed.bench.ts [deleted file]
packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/__tests__/deferredComputed.spec.ts [deleted file]
packages/reactivity/__tests__/effect.spec.ts
packages/reactivity/__tests__/effectScope.spec.ts
packages/reactivity/__tests__/gc.spec.ts
packages/reactivity/__tests__/reactiveObject.bench.ts [deleted file]
packages/reactivity/__tests__/readonly.spec.ts
packages/reactivity/__tests__/ref.spec.ts
packages/reactivity/__tests__/shallowReactive.spec.ts
packages/reactivity/src/baseHandlers.ts
packages/reactivity/src/collectionHandlers.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/deferredComputed.ts [deleted file]
packages/reactivity/src/dep.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/index.ts
packages/reactivity/src/reactiveEffect.ts [deleted file]
packages/reactivity/src/ref.ts
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
packages/runtime-core/__tests__/apiWatch.spec.ts
packages/runtime-core/src/apiAsyncComponent.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentPublicInstance.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/customFormatter.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/scheduler.ts
packages/server-renderer/__tests__/ssrComputed.spec.ts
packages/server-renderer/src/render.ts

diff --git a/packages/reactivity/__benchmarks__/computed.bench.ts b/packages/reactivity/__benchmarks__/computed.bench.ts
new file mode 100644 (file)
index 0000000..d975750
--- /dev/null
@@ -0,0 +1,200 @@
+import { bench, describe } from 'vitest'
+import { type ComputedRef, type Ref, computed, effect, ref } from '../src'
+
+describe('computed', () => {
+  bench('create computed', () => {
+    computed(() => 100)
+  })
+
+  {
+    const v = ref(100)
+    computed(() => v.value * 2)
+    let i = 0
+    bench("write ref, don't read computed (without effect)", () => {
+      v.value = i++
+    })
+  }
+
+  {
+    const v = ref(100)
+    const c = computed(() => {
+      return v.value * 2
+    })
+    effect(() => c.value)
+    let i = 0
+    bench("write ref, don't read computed (with effect)", () => {
+      v.value = i++
+    })
+  }
+
+  {
+    const v = ref(100)
+    const c = computed(() => {
+      return v.value * 2
+    })
+    let i = 0
+    bench('write ref, read computed (without effect)', () => {
+      v.value = i++
+      c.value
+    })
+  }
+
+  {
+    const v = ref(100)
+    const c = computed(() => {
+      return v.value * 2
+    })
+    effect(() => c.value)
+    let i = 0
+    bench('write ref, read computed (with effect)', () => {
+      v.value = i++
+      c.value
+    })
+  }
+
+  {
+    const v = ref(100)
+    const computeds: ComputedRef<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      const c = computed(() => {
+        return v.value * 2
+      })
+      computeds.push(c)
+    }
+    let i = 0
+    bench("write ref, don't read 1000 computeds (without effect)", () => {
+      v.value = i++
+    })
+  }
+
+  {
+    const v = ref(100)
+    const computeds: ComputedRef<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      const c = computed(() => {
+        return v.value * 2
+      })
+      effect(() => c.value)
+      computeds.push(c)
+    }
+    let i = 0
+    bench(
+      "write ref, don't read 1000 computeds (with multiple effects)",
+      () => {
+        v.value = i++
+      },
+    )
+  }
+
+  {
+    const v = ref(100)
+    const computeds: ComputedRef<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      const c = computed(() => {
+        return v.value * 2
+      })
+      computeds.push(c)
+    }
+    effect(() => {
+      for (let i = 0; i < 1000; i++) {
+        computeds[i].value
+      }
+    })
+    let i = 0
+    bench("write ref, don't read 1000 computeds (with single effect)", () => {
+      v.value = i++
+    })
+  }
+
+  {
+    const v = ref(100)
+    const computeds: ComputedRef<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      const c = computed(() => {
+        return v.value * 2
+      })
+      computeds.push(c)
+    }
+    let i = 0
+    bench('write ref, read 1000 computeds (no effect)', () => {
+      v.value = i++
+      computeds.forEach(c => c.value)
+    })
+  }
+
+  {
+    const v = ref(100)
+    const computeds: ComputedRef<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      const c = computed(() => {
+        return v.value * 2
+      })
+      effect(() => c.value)
+      computeds.push(c)
+    }
+    let i = 0
+    bench('write ref, read 1000 computeds (with multiple effects)', () => {
+      v.value = i++
+      computeds.forEach(c => c.value)
+    })
+  }
+
+  {
+    const v = ref(100)
+    const computeds: ComputedRef<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      const c = computed(() => {
+        return v.value * 2
+      })
+      effect(() => c.value)
+      computeds.push(c)
+    }
+    effect(() => {
+      for (let i = 0; i < 1000; i++) {
+        computeds[i].value
+      }
+    })
+    let i = 0
+    bench('write ref, read 1000 computeds (with single effect)', () => {
+      v.value = i++
+      computeds.forEach(c => c.value)
+    })
+  }
+
+  {
+    const refs: Ref<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      refs.push(ref(i))
+    }
+    const c = computed(() => {
+      let total = 0
+      refs.forEach(ref => (total += ref.value))
+      return total
+    })
+    let i = 0
+    const n = refs.length
+    bench('1000 refs, read 1 computed (without effect)', () => {
+      refs[i++ % n].value++
+      c.value
+    })
+  }
+
+  {
+    const refs: Ref<number>[] = []
+    for (let i = 0, n = 1000; i < n; i++) {
+      refs.push(ref(i))
+    }
+    const c = computed(() => {
+      let total = 0
+      refs.forEach(ref => (total += ref.value))
+      return total
+    })
+    effect(() => c.value)
+    let i = 0
+    const n = refs.length
+    bench('1000 refs, read 1 computed (with effect)', () => {
+      refs[i++ % n].value++
+      c.value
+    })
+  }
+})
diff --git a/packages/reactivity/__benchmarks__/effect.bench.ts b/packages/reactivity/__benchmarks__/effect.bench.ts
new file mode 100644 (file)
index 0000000..8d3d6ec
--- /dev/null
@@ -0,0 +1,111 @@
+import { bench, describe } from 'vitest'
+import { type Ref, effect, ref } from '../src'
+
+describe('effect', () => {
+  {
+    let i = 0
+    const n = ref(0)
+    effect(() => n.value)
+    bench('single ref invoke', () => {
+      n.value = i++
+    })
+  }
+
+  function benchEffectCreate(size: number) {
+    bench(`create an effect that tracks ${size} refs`, () => {
+      const refs: Ref[] = []
+      for (let i = 0; i < size; i++) {
+        refs.push(ref(i))
+      }
+      effect(() => {
+        for (let i = 0; i < size; i++) {
+          refs[i].value
+        }
+      })
+    })
+  }
+
+  benchEffectCreate(1)
+  benchEffectCreate(10)
+  benchEffectCreate(100)
+  benchEffectCreate(1000)
+
+  function benchEffectCreateAndStop(size: number) {
+    bench(`create and stop an effect that tracks ${size} refs`, () => {
+      const refs: Ref[] = []
+      for (let i = 0; i < size; i++) {
+        refs.push(ref(i))
+      }
+      const e = effect(() => {
+        for (let i = 0; i < size; i++) {
+          refs[i].value
+        }
+      })
+      e.effect.stop()
+    })
+  }
+
+  benchEffectCreateAndStop(1)
+  benchEffectCreateAndStop(10)
+  benchEffectCreateAndStop(100)
+  benchEffectCreateAndStop(1000)
+
+  function benchWithRefs(size: number) {
+    let j = 0
+    const refs: Ref[] = []
+    for (let i = 0; i < size; i++) {
+      refs.push(ref(i))
+    }
+    effect(() => {
+      for (let i = 0; i < size; i++) {
+        refs[i].value
+      }
+    })
+    bench(`1 effect, mutate ${size} refs`, () => {
+      for (let i = 0; i < size; i++) {
+        refs[i].value = i + j++
+      }
+    })
+  }
+
+  benchWithRefs(10)
+  benchWithRefs(100)
+  benchWithRefs(1000)
+
+  function benchWithBranches(size: number) {
+    const toggle = ref(true)
+    const refs: Ref[] = []
+    for (let i = 0; i < size; i++) {
+      refs.push(ref(i))
+    }
+    effect(() => {
+      if (toggle.value) {
+        for (let i = 0; i < size; i++) {
+          refs[i].value
+        }
+      }
+    })
+    bench(`${size} refs branch toggle`, () => {
+      toggle.value = !toggle.value
+    })
+  }
+
+  benchWithBranches(10)
+  benchWithBranches(100)
+  benchWithBranches(1000)
+
+  function benchMultipleEffects(size: number) {
+    let i = 0
+    const n = ref(0)
+    for (let i = 0; i < size; i++) {
+      effect(() => n.value)
+    }
+    bench(`1 ref invoking ${size} effects`, () => {
+      n.value = i++
+    })
+  }
+
+  benchMultipleEffects(10)
+  benchMultipleEffects(100)
+  benchMultipleEffects(1000)
+})
similarity index 94%
rename from packages/reactivity/__tests__/reactiveArray.bench.ts
rename to packages/reactivity/__benchmarks__/reactiveArray.bench.ts
index 9ce0dc531d15f738a1b3c767ded8d3045cf3d11d..6726cccfd8937e462c75b8273ae6740b1274503c 100644 (file)
@@ -3,7 +3,7 @@ import { computed, reactive, readonly, shallowRef, triggerRef } from '../src'
 
 for (let amount = 1e1; amount < 1e4; amount *= 10) {
   {
-    const rawArray = []
+    const rawArray: any[] = []
     for (let i = 0, n = amount; i < n; i++) {
       rawArray.push(i)
     }
@@ -21,7 +21,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
   }
 
   {
-    const rawArray = []
+    const rawArray: any[] = []
     for (let i = 0, n = amount; i < n; i++) {
       rawArray.push(i)
     }
@@ -40,7 +40,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
   }
 
   {
-    const rawArray = []
+    const rawArray: any[] = []
     for (let i = 0, n = amount; i < n; i++) {
       rawArray.push(i)
     }
@@ -56,7 +56,7 @@ for (let amount = 1e1; amount < 1e4; amount *= 10) {
   }
 
   {
-    const rawArray = []
+    const rawArray: any[] = []
     for (let i = 0, n = amount; i < n; i++) {
       rawArray.push(i)
     }
similarity index 97%
rename from packages/reactivity/__tests__/reactiveMap.bench.ts
rename to packages/reactivity/__benchmarks__/reactiveMap.bench.ts
index 70a034e96c38508a22778f4d45f575b079d55d35..f8b4611153e554de9882f560c927ba632477cf95 100644 (file)
@@ -79,7 +79,7 @@ bench('create reactive map', () => {
 
 {
   const r = reactive(createMap({ a: 1 }))
-  const computeds = []
+  const computeds: any[] = []
   for (let i = 0, n = 1000; i < n; i++) {
     const c = computed(() => {
       return r.get('a') * 2
@@ -94,7 +94,7 @@ bench('create reactive map', () => {
 
 {
   const r = reactive(createMap({ a: 1 }))
-  const computeds = []
+  const computeds: any[] = []
   for (let i = 0, n = 1000; i < n; i++) {
     const c = computed(() => {
       return r.get('a') * 2
diff --git a/packages/reactivity/__benchmarks__/reactiveObject.bench.ts b/packages/reactivity/__benchmarks__/reactiveObject.bench.ts
new file mode 100644 (file)
index 0000000..a326a11
--- /dev/null
@@ -0,0 +1,21 @@
+import { bench } from 'vitest'
+import { reactive } from '../src'
+
+bench('create reactive obj', () => {
+  reactive({ a: 1 })
+})
+
+{
+  const r = reactive({ a: 1 })
+  bench('read reactive obj property', () => {
+    r.a
+  })
+}
+
+{
+  let i = 0
+  const r = reactive({ a: 1 })
+  bench('write reactive obj property', () => {
+    r.a = i++
+  })
+}
similarity index 99%
rename from packages/reactivity/__tests__/ref.bench.ts
rename to packages/reactivity/__benchmarks__/ref.bench.ts
index 286d53e884024aa1f2427db936428de7bf1445be..0c05890179b07b8356c20ff26e146005b95f5224 100644 (file)
@@ -26,7 +26,6 @@ describe('ref', () => {
     const v = ref(100)
     bench('write/read ref', () => {
       v.value = i++
-
       v.value
     })
   }
diff --git a/packages/reactivity/__tests__/computed.bench.ts b/packages/reactivity/__tests__/computed.bench.ts
deleted file mode 100644 (file)
index 0ffa288..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-import { bench, describe } from 'vitest'
-import { type ComputedRef, type Ref, computed, ref } from '../src/index'
-
-describe('computed', () => {
-  bench('create computed', () => {
-    computed(() => 100)
-  })
-
-  {
-    let i = 0
-    const o = ref(100)
-    bench('write independent ref dep', () => {
-      o.value = i++
-    })
-  }
-
-  {
-    const v = ref(100)
-    computed(() => v.value * 2)
-    let i = 0
-    bench("write ref, don't read computed (never invoked)", () => {
-      v.value = i++
-    })
-  }
-
-  {
-    const v = ref(100)
-    computed(() => {
-      return v.value * 2
-    })
-    let i = 0
-    bench("write ref, don't read computed (never invoked)", () => {
-      v.value = i++
-    })
-  }
-
-  {
-    const v = ref(100)
-    const c = computed(() => {
-      return v.value * 2
-    })
-    c.value
-    let i = 0
-    bench("write ref, don't read computed (invoked)", () => {
-      v.value = i++
-    })
-  }
-
-  {
-    const v = ref(100)
-    const c = computed(() => {
-      return v.value * 2
-    })
-    let i = 0
-    bench('write ref, read computed', () => {
-      v.value = i++
-      c.value
-    })
-  }
-
-  {
-    const v = ref(100)
-    const computeds = []
-    for (let i = 0, n = 1000; i < n; i++) {
-      const c = computed(() => {
-        return v.value * 2
-      })
-      computeds.push(c)
-    }
-    let i = 0
-    bench("write ref, don't read 1000 computeds (never invoked)", () => {
-      v.value = i++
-    })
-  }
-
-  {
-    const v = ref(100)
-    const computeds = []
-    for (let i = 0, n = 1000; i < n; i++) {
-      const c = computed(() => {
-        return v.value * 2
-      })
-      c.value
-      computeds.push(c)
-    }
-    let i = 0
-    bench("write ref, don't read 1000 computeds (invoked)", () => {
-      v.value = i++
-    })
-  }
-
-  {
-    const v = ref(100)
-    const computeds: ComputedRef<number>[] = []
-    for (let i = 0, n = 1000; i < n; i++) {
-      const c = computed(() => {
-        return v.value * 2
-      })
-      c.value
-      computeds.push(c)
-    }
-    let i = 0
-    bench('write ref, read 1000 computeds', () => {
-      v.value = i++
-      computeds.forEach(c => c.value)
-    })
-  }
-
-  {
-    const refs: Ref<number>[] = []
-    for (let i = 0, n = 1000; i < n; i++) {
-      refs.push(ref(i))
-    }
-    const c = computed(() => {
-      let total = 0
-      refs.forEach(ref => (total += ref.value))
-      return total
-    })
-    let i = 0
-    const n = refs.length
-    bench('1000 refs, 1 computed', () => {
-      refs[i++ % n].value++
-      c.value
-    })
-  }
-})
index c9f47720eddc429f9f273024a8b1187fee63a1cd..e2325be54d2d190ba074a4d85dc6404ebd82a62a 100644 (file)
@@ -1,4 +1,12 @@
-import { h, nextTick, nodeOps, render, serializeInner } from '@vue/runtime-test'
+import {
+  h,
+  nextTick,
+  nodeOps,
+  onMounted,
+  onUnmounted,
+  render,
+  serializeInner,
+} from '@vue/runtime-test'
 import {
   type DebuggerEvent,
   ITERATE_KEY,
@@ -13,8 +21,8 @@ import {
   shallowRef,
   toRaw,
 } from '../src'
-import { DirtyLevels } from '../src/constants'
-import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed'
+import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
+import type { ComputedRef, ComputedRefImpl } from '../src/computed'
 
 describe('reactivity/computed', () => {
   it('should return updated value', () => {
@@ -123,21 +131,6 @@ describe('reactivity/computed', () => {
     expect(getter2).toHaveBeenCalledTimes(2)
   })
 
-  it('should no longer update when stopped', () => {
-    const value = reactive<{ foo?: number }>({})
-    const cValue = computed(() => value.foo)
-    let dummy
-    effect(() => {
-      dummy = cValue.value
-    })
-    expect(dummy).toBe(undefined)
-    value.foo = 1
-    expect(dummy).toBe(1)
-    cValue.effect.stop()
-    value.foo = 2
-    expect(dummy).toBe(1)
-  })
-
   it('should support setter', () => {
     const n = ref(1)
     const plusOne = computed({
@@ -219,12 +212,6 @@ describe('reactivity/computed', () => {
     expect(isReadonly(z.value.a)).toBe(false)
   })
 
-  it('should expose value when stopped', () => {
-    const x = computed(() => 1)
-    x.effect.stop()
-    expect(x.value).toBe(1)
-  })
-
   it('debug: onTrack', () => {
     let events: DebuggerEvent[] = []
     const onTrack = vi.fn((e: DebuggerEvent) => {
@@ -238,19 +225,19 @@ describe('reactivity/computed', () => {
     expect(onTrack).toHaveBeenCalledTimes(3)
     expect(events).toEqual([
       {
-        effect: c.effect,
+        effect: c,
         target: toRaw(obj),
         type: TrackOpTypes.GET,
         key: 'foo',
       },
       {
-        effect: c.effect,
+        effect: c,
         target: toRaw(obj),
         type: TrackOpTypes.HAS,
         key: 'bar',
       },
       {
-        effect: c.effect,
+        effect: c,
         target: toRaw(obj),
         type: TrackOpTypes.ITERATE,
         key: ITERATE_KEY,
@@ -266,14 +253,14 @@ describe('reactivity/computed', () => {
     const obj = reactive<{ foo?: number }>({ foo: 1 })
     const c = computed(() => obj.foo, { onTrigger })
 
-    // computed won't trigger compute until accessed
-    c.value
+    // computed won't track until it has a subscriber
+    effect(() => c.value)
 
     obj.foo!++
     expect(c.value).toBe(2)
     expect(onTrigger).toHaveBeenCalledTimes(1)
     expect(events[0]).toEqual({
-      effect: c.effect,
+      effect: c,
       target: toRaw(obj),
       type: TriggerOpTypes.SET,
       key: 'foo',
@@ -285,7 +272,7 @@ describe('reactivity/computed', () => {
     expect(c.value).toBeUndefined()
     expect(onTrigger).toHaveBeenCalledTimes(2)
     expect(events[1]).toEqual({
-      effect: c.effect,
+      effect: c,
       target: toRaw(obj),
       type: TriggerOpTypes.DELETE,
       key: 'foo',
@@ -380,17 +367,17 @@ describe('reactivity/computed', () => {
     const a = ref(0)
     const b = computed(() => {
       return a.value % 3 !== 0
-    })
+    }) as unknown as ComputedRefImpl
     const c = computed(() => {
       cSpy()
       if (a.value % 3 === 2) {
         return 'expensive'
       }
       return 'cheap'
-    })
+    }) as unknown as ComputedRefImpl
     const d = computed(() => {
       return a.value % 3 === 2
-    })
+    }) as unknown as ComputedRefImpl
     const e = computed(() => {
       if (b.value) {
         if (d.value) {
@@ -398,16 +385,15 @@ describe('reactivity/computed', () => {
         }
       }
       return c.value
-    })
+    }) as unknown as ComputedRefImpl
 
     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(e.deps!.dep).toBe(b.dep)
+    expect(e.deps!.nextDep!.dep).toBe(d.dep)
+    expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
     expect(cSpy).toHaveBeenCalledTimes(2)
 
     a.value++
@@ -456,17 +442,14 @@ describe('reactivity/computed', () => {
     expect(fnSpy).toBeCalledTimes(2)
   })
 
-  it('should chained recurse effects clear dirty after trigger', () => {
+  it('should chained recursive effects clear dirty after trigger', () => {
     const v = ref(1)
-    const c1 = computed(() => v.value)
-    const c2 = computed(() => c1.value)
+    const c1 = computed(() => v.value) as unknown as ComputedRefImpl
+    const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
 
-    c1.effect.allowRecurse = true
-    c2.effect.allowRecurse = true
     c2.value
-
-    expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
-    expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty)
+    expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
+    expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
   })
 
   it('should chained computeds dirtyLevel update with first computed effect', () => {
@@ -481,15 +464,7 @@ describe('reactivity/computed', () => {
     const c3 = computed(() => c2.value)
 
     c3.value
-
-    expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
-    expect(c2.effect._dirtyLevel).toBe(
-      DirtyLevels.MaybeDirty_ComputedSideEffect,
-    )
-    expect(c3.effect._dirtyLevel).toBe(
-      DirtyLevels.MaybeDirty_ComputedSideEffect,
-    )
-    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+    // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should work when chained(ref+computed)', () => {
@@ -502,9 +477,8 @@ describe('reactivity/computed', () => {
     })
     const c2 = computed(() => v.value + c1.value)
     expect(c2.value).toBe('0foo')
-    expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
     expect(c2.value).toBe('1foo')
-    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+    // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should trigger effect even computed already dirty', () => {
@@ -519,15 +493,16 @@ describe('reactivity/computed', () => {
     const c2 = computed(() => v.value + c1.value)
 
     effect(() => {
-      fnSpy()
-      c2.value
+      fnSpy(c2.value)
     })
     expect(fnSpy).toBeCalledTimes(1)
-    expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
-    expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
+    expect(fnSpy.mock.calls).toMatchObject([['0foo']])
+    expect(v.value).toBe(1)
     v.value = 2
     expect(fnSpy).toBeCalledTimes(2)
-    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+    expect(fnSpy.mock.calls).toMatchObject([['0foo'], ['2foo']])
+    expect(v.value).toBe(2)
+    // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   // #10185
@@ -553,25 +528,12 @@ describe('reactivity/computed', () => {
 
     c3.value
     v2.value = true
-    expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
-    expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
 
     c3.value
-    expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
-    expect(c2.effect._dirtyLevel).toBe(
-      DirtyLevels.MaybeDirty_ComputedSideEffect,
-    )
-    expect(c3.effect._dirtyLevel).toBe(
-      DirtyLevels.MaybeDirty_ComputedSideEffect,
-    )
-
     v1.value.v.value = 999
-    expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
-    expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
-    expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
 
     expect(c3.value).toBe('yes')
-    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+    // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
@@ -593,10 +555,10 @@ describe('reactivity/computed', () => {
     await nextTick()
     await nextTick()
     expect(serializeInner(root)).toBe(`2`)
-    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+    // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
-  it('should not trigger effect scheduler by recurse computed effect', async () => {
+  it('should not trigger effect scheduler by recursive computed effect', async () => {
     const v = ref('Hello')
     const c = computed(() => {
       v.value += ' World'
@@ -615,7 +577,279 @@ describe('reactivity/computed', () => {
 
     v.value += ' World'
     await nextTick()
-    expect(serializeInner(root)).toBe('Hello World World World World')
-    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+    expect(serializeInner(root)).toBe('Hello World World World')
+    // expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
+  })
+
+  test('should not trigger if value did not change', () => {
+    const src = ref(0)
+    const c = computed(() => src.value % 2)
+    const spy = vi.fn()
+    effect(() => {
+      spy(c.value)
+    })
+    expect(spy).toHaveBeenCalledTimes(1)
+    src.value = 2
+
+    // should not trigger
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    src.value = 3
+    src.value = 5
+    // should trigger because latest value changes
+    expect(spy).toHaveBeenCalledTimes(2)
+  })
+
+  test('chained computed trigger', () => {
+    const effectSpy = vi.fn()
+    const c1Spy = vi.fn()
+    const c2Spy = vi.fn()
+
+    const src = ref(0)
+    const c1 = computed(() => {
+      c1Spy()
+      return src.value % 2
+    })
+    const c2 = computed(() => {
+      c2Spy()
+      return c1.value + 1
+    })
+
+    effect(() => {
+      effectSpy(c2.value)
+    })
+
+    expect(c1Spy).toHaveBeenCalledTimes(1)
+    expect(c2Spy).toHaveBeenCalledTimes(1)
+    expect(effectSpy).toHaveBeenCalledTimes(1)
+
+    src.value = 1
+    expect(c1Spy).toHaveBeenCalledTimes(2)
+    expect(c2Spy).toHaveBeenCalledTimes(2)
+    expect(effectSpy).toHaveBeenCalledTimes(2)
+  })
+
+  test('chained computed avoid re-compute', () => {
+    const effectSpy = vi.fn()
+    const c1Spy = vi.fn()
+    const c2Spy = vi.fn()
+
+    const src = ref(0)
+    const c1 = computed(() => {
+      c1Spy()
+      return src.value % 2
+    })
+    const c2 = computed(() => {
+      c2Spy()
+      return c1.value + 1
+    })
+
+    effect(() => {
+      effectSpy(c2.value)
+    })
+
+    expect(effectSpy).toHaveBeenCalledTimes(1)
+    src.value = 2
+    src.value = 4
+    src.value = 6
+    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', () => {
+    const effectSpy = vi.fn()
+    const c1Spy = vi.fn()
+    const c2Spy = vi.fn()
+
+    const src = ref(0)
+    const c1 = computed(() => {
+      c1Spy()
+      return src.value % 2
+    })
+    const c2 = computed(() => {
+      c2Spy()
+      return c1.value + 1
+    })
+
+    effect(() => {
+      effectSpy(c2.value)
+    })
+
+    expect(effectSpy).toHaveBeenCalledTimes(1)
+    expect(effectSpy).toHaveBeenCalledWith(1)
+    expect(c2.value).toBe(1)
+
+    expect(c1Spy).toHaveBeenCalledTimes(1)
+    expect(c2Spy).toHaveBeenCalledTimes(1)
+
+    src.value = 1
+    // value should be available sync
+    expect(c2.value).toBe(2)
+    expect(c2Spy).toHaveBeenCalledTimes(2)
+  })
+
+  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 = computed(() => {
+      c1Spy()
+      return src.value % 2
+    })
+    const c2 = computed(() => {
+      c2Spy()
+      return c1.value + 1
+    })
+
+    effect(() => {
+      effectSpy(c2.value)
+    })
+    expect(effectSpy).toHaveBeenCalledTimes(1)
+
+    src.value = 1
+    // sync access c2
+    c2.value
+    expect(effectSpy).toHaveBeenCalledTimes(2)
+  })
+
+  it('computed should force track in untracked zone', () => {
+    const n = ref(0)
+    const spy1 = vi.fn()
+    const spy2 = vi.fn()
+
+    let c: ComputedRef
+    effect(() => {
+      spy1()
+      pauseTracking()
+      n.value
+      c = computed(() => n.value + 1)
+      // access computed now to force refresh
+      c.value
+      effect(() => spy2(c.value))
+      n.value
+      resetTracking()
+    })
+
+    expect(spy1).toHaveBeenCalledTimes(1)
+    expect(spy2).toHaveBeenCalledTimes(1)
+
+    n.value++
+    // outer effect should not trigger
+    expect(spy1).toHaveBeenCalledTimes(1)
+    // inner effect should trigger
+    expect(spy2).toHaveBeenCalledTimes(2)
+  })
+
+  // not recommended behavior, but needed for backwards compatibility
+  // used in VueUse asyncComputed
+  it('computed side effect should be able trigger', () => {
+    const a = ref(false)
+    const b = ref(false)
+    const c = computed(() => {
+      a.value = true
+      return b.value
+    })
+    effect(() => {
+      if (a.value) {
+        b.value = true
+      }
+    })
+    expect(b.value).toBe(false)
+    // accessing c triggers change
+    c.value
+    expect(b.value).toBe(true)
+    expect(c.value).toBe(true)
+  })
+
+  it('chained computed should work when accessed before having subs', () => {
+    const n = ref(0)
+    const c = computed(() => n.value)
+    const d = computed(() => c.value + 1)
+    const spy = vi.fn()
+
+    // access
+    d.value
+
+    let dummy
+    effect(() => {
+      spy()
+      dummy = d.value
+    })
+    expect(spy).toHaveBeenCalledTimes(1)
+    expect(dummy).toBe(1)
+
+    n.value++
+    expect(spy).toHaveBeenCalledTimes(2)
+    expect(dummy).toBe(2)
+  })
+
+  // #10236
+  it('chained computed should still refresh after owner component unmount', async () => {
+    const a = ref(0)
+    const spy = vi.fn()
+
+    const Child = {
+      setup() {
+        const b = computed(() => a.value + 1)
+        const c = computed(() => b.value + 1)
+        // access
+        c.value
+        onUnmounted(() => spy(c.value))
+        return () => {}
+      },
+    }
+
+    const show = ref(true)
+    const Parent = {
+      setup() {
+        return () => (show.value ? h(Child) : null)
+      },
+    }
+
+    render(h(Parent), nodeOps.createElement('div'))
+
+    a.value++
+    show.value = false
+
+    await nextTick()
+    expect(spy).toHaveBeenCalledWith(3)
+  })
+
+  // case: radix-vue `useForwardExpose` sets a template ref during mount,
+  // and checks for the element's closest form element in a computed.
+  // the computed is expected to only evaluate after mount.
+  it('computed deps should only be refreshed when the subscribing effect is run, not when scheduled', async () => {
+    const calls: string[] = []
+    const a = ref(0)
+    const b = computed(() => {
+      calls.push('b eval')
+      return a.value + 1
+    })
+
+    const App = {
+      setup() {
+        onMounted(() => {
+          calls.push('mounted')
+        })
+        return () =>
+          h(
+            'div',
+            {
+              ref: () => (a.value = 1),
+            },
+            b.value,
+          )
+      },
+    }
+
+    render(h(App), nodeOps.createElement('div'))
+
+    await nextTick()
+    expect(calls).toMatchObject(['b eval', 'mounted', 'b eval'])
   })
 })
diff --git a/packages/reactivity/__tests__/deferredComputed.spec.ts b/packages/reactivity/__tests__/deferredComputed.spec.ts
deleted file mode 100644 (file)
index 8e78ba9..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-import { computed, effect, ref } from '../src'
-
-describe('deferred computed', () => {
-  test('should not trigger if value did not change', () => {
-    const src = ref(0)
-    const c = computed(() => src.value % 2)
-    const spy = vi.fn()
-    effect(() => {
-      spy(c.value)
-    })
-    expect(spy).toHaveBeenCalledTimes(1)
-    src.value = 2
-
-    // should not trigger
-    expect(spy).toHaveBeenCalledTimes(1)
-
-    src.value = 3
-    src.value = 5
-    // should trigger because latest value changes
-    expect(spy).toHaveBeenCalledTimes(2)
-  })
-
-  test('chained computed trigger', () => {
-    const effectSpy = vi.fn()
-    const c1Spy = vi.fn()
-    const c2Spy = vi.fn()
-
-    const src = ref(0)
-    const c1 = computed(() => {
-      c1Spy()
-      return src.value % 2
-    })
-    const c2 = computed(() => {
-      c2Spy()
-      return c1.value + 1
-    })
-
-    effect(() => {
-      effectSpy(c2.value)
-    })
-
-    expect(c1Spy).toHaveBeenCalledTimes(1)
-    expect(c2Spy).toHaveBeenCalledTimes(1)
-    expect(effectSpy).toHaveBeenCalledTimes(1)
-
-    src.value = 1
-    expect(c1Spy).toHaveBeenCalledTimes(2)
-    expect(c2Spy).toHaveBeenCalledTimes(2)
-    expect(effectSpy).toHaveBeenCalledTimes(2)
-  })
-
-  test('chained computed avoid re-compute', () => {
-    const effectSpy = vi.fn()
-    const c1Spy = vi.fn()
-    const c2Spy = vi.fn()
-
-    const src = ref(0)
-    const c1 = computed(() => {
-      c1Spy()
-      return src.value % 2
-    })
-    const c2 = computed(() => {
-      c2Spy()
-      return c1.value + 1
-    })
-
-    effect(() => {
-      effectSpy(c2.value)
-    })
-
-    expect(effectSpy).toHaveBeenCalledTimes(1)
-    src.value = 2
-    src.value = 4
-    src.value = 6
-    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', () => {
-    const effectSpy = vi.fn()
-    const c1Spy = vi.fn()
-    const c2Spy = vi.fn()
-
-    const src = ref(0)
-    const c1 = computed(() => {
-      c1Spy()
-      return src.value % 2
-    })
-    const c2 = computed(() => {
-      c2Spy()
-      return c1.value + 1
-    })
-
-    effect(() => {
-      effectSpy(c2.value)
-    })
-
-    expect(effectSpy).toHaveBeenCalledTimes(1)
-    expect(effectSpy).toHaveBeenCalledWith(1)
-    expect(c2.value).toBe(1)
-
-    expect(c1Spy).toHaveBeenCalledTimes(1)
-    expect(c2Spy).toHaveBeenCalledTimes(1)
-
-    src.value = 1
-    // value should be available sync
-    expect(c2.value).toBe(2)
-    expect(c2Spy).toHaveBeenCalledTimes(2)
-  })
-
-  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 = computed(() => {
-      c1Spy()
-      return src.value % 2
-    })
-    const c2 = computed(() => {
-      c2Spy()
-      return c1.value + 1
-    })
-
-    effect(() => {
-      effectSpy(c2.value)
-    })
-    expect(effectSpy).toHaveBeenCalledTimes(1)
-
-    src.value = 1
-    // sync access c2
-    c2.value
-    expect(effectSpy).toHaveBeenCalledTimes(2)
-  })
-
-  test('should not compute if deactivated before scheduler is called', () => {
-    const c1Spy = vi.fn()
-    const src = ref(0)
-    const c1 = computed(() => {
-      c1Spy()
-      return src.value % 2
-    })
-    effect(() => c1.value)
-    expect(c1Spy).toHaveBeenCalledTimes(1)
-
-    c1.effect.stop()
-    // trigger
-    src.value++
-    expect(c1Spy).toHaveBeenCalledTimes(1)
-  })
-})
index bd26934f1cee760fac7a4a2c491703a580df4e12..99453d35d87e88be3ad5f750f00c5e3fb97d687a 100644 (file)
@@ -11,8 +11,7 @@ import {
   stop,
   toRaw,
 } from '../src/index'
-import { pauseScheduling, resetScheduling } from '../src/effect'
-import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect'
+import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
 import {
   computed,
   h,
@@ -22,6 +21,12 @@ import {
   render,
   serializeInner,
 } from '@vue/runtime-test'
+import {
+  endBatch,
+  pauseTracking,
+  resetTracking,
+  startBatch,
+} from '../src/effect'
 
 describe('reactivity/effect', () => {
   it('should run the passed function once (wrapped by a effect)', () => {
@@ -698,18 +703,6 @@ describe('reactivity/effect', () => {
     expect(dummy).toBe(1)
   })
 
-  it('lazy', () => {
-    const obj = reactive({ foo: 1 })
-    let dummy
-    const runner = effect(() => (dummy = obj.foo), { lazy: true })
-    expect(dummy).toBe(undefined)
-
-    expect(runner()).toBe(1)
-    expect(dummy).toBe(1)
-    obj.foo = 2
-    expect(dummy).toBe(2)
-  })
-
   it('scheduler', () => {
     let dummy
     let run: any
@@ -1005,7 +998,7 @@ describe('reactivity/effect', () => {
     })
   })
 
-  it('should be triggered once with pauseScheduling', () => {
+  it('should be triggered once with batching', () => {
     const counter = reactive({ num: 0 })
 
     const counterSpy = vi.fn(() => counter.num)
@@ -1013,10 +1006,10 @@ describe('reactivity/effect', () => {
 
     counterSpy.mockClear()
 
-    pauseScheduling()
+    startBatch()
     counter.num++
     counter.num++
-    resetScheduling()
+    endBatch()
     expect(counterSpy).toHaveBeenCalledTimes(1)
   })
 
@@ -1049,47 +1042,76 @@ describe('reactivity/effect', () => {
     expect(renderSpy).toHaveBeenCalledTimes(2)
   })
 
-  describe('empty dep cleanup', () => {
+  it('nested effect should force track in untracked zone', () => {
+    const n = ref(0)
+    const spy1 = vi.fn()
+    const spy2 = vi.fn()
+
+    effect(() => {
+      spy1()
+      pauseTracking()
+      n.value
+      effect(() => {
+        n.value
+        spy2()
+      })
+      n.value
+      resetTracking()
+    })
+
+    expect(spy1).toHaveBeenCalledTimes(1)
+    expect(spy2).toHaveBeenCalledTimes(1)
+
+    n.value++
+    // outer effect should not trigger
+    expect(spy1).toHaveBeenCalledTimes(1)
+    // inner effect should trigger
+    expect(spy2).toHaveBeenCalledTimes(2)
+  })
+
+  describe('dep unsubscribe', () => {
+    function getSubCount(dep: Dep | undefined) {
+      let count = 0
+      let sub = dep!.subs
+      while (sub) {
+        count++
+        sub = sub.prevSub
+      }
+      return count
+    }
+
     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)
+      expect(getSubCount(dep)).toBe(1)
       obj.prop = 2
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
-      expect(dep).toHaveLength(1)
+      expect(getSubCount(dep)).toBe(1)
       stop(runner)
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+      expect(getSubCount(dep)).toBe(0)
       obj.prop = 3
       runner()
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+      expect(getSubCount(dep)).toBe(0)
     })
 
     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)
+      expect(getSubCount(dep)).toBe(1)
       const runner2 = effect(() => obj.prop)
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
-      expect(dep).toHaveLength(2)
+      expect(getSubCount(dep)).toBe(2)
       obj.prop = 2
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
-      expect(dep).toHaveLength(2)
+      expect(getSubCount(dep)).toBe(2)
       stop(runner1)
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
-      expect(dep).toHaveLength(1)
+      expect(getSubCount(dep)).toBe(1)
       obj.prop = 3
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBe(dep)
-      expect(dep).toHaveLength(1)
+      expect(getSubCount(dep)).toBe(1)
       stop(runner2)
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
       obj.prop = 4
       runner1()
       runner2()
-      expect(getDepFromReactive(toRaw(obj), 'prop')).toBeUndefined()
+      expect(getSubCount(dep)).toBe(0)
     })
 
     it('should remove the dep when it is no longer used by the effect', () => {
@@ -1098,18 +1120,15 @@ describe('reactivity/effect', () => {
         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)
+      expect(getSubCount(getDepFromReactive(toRaw(obj), 'a'))).toBe(1)
+      expect(getSubCount(depC)).toBe(1)
       obj.c = 'b'
       obj.a = 4
-      expect(getDepFromReactive(toRaw(obj), 'a')).toBeUndefined()
-      expect(getDepFromReactive(toRaw(obj), 'b')).toHaveLength(1)
+      expect(getSubCount(getDepFromReactive(toRaw(obj), 'b'))).toBe(1)
       expect(getDepFromReactive(toRaw(obj), 'c')).toBe(depC)
-      expect(depC).toHaveLength(1)
+      expect(getSubCount(depC)).toBe(1)
     })
   })
 })
index f7e3241ccd6923280bbd2a89b3653784e91dde7c..8a7f26dbb2dd418a9e818d7009a88cb5fb548394 100644 (file)
@@ -247,16 +247,15 @@ describe('reactivity/effect/scope', () => {
       watchEffect(() => {
         watchEffectSpy()
         r.value
+        c.value
       })
     })
 
-    c!.value // computed is lazy so trigger collection
     expect(computedSpy).toHaveBeenCalledTimes(1)
     expect(watchSpy).toHaveBeenCalledTimes(0)
     expect(watchEffectSpy).toHaveBeenCalledTimes(1)
 
     r.value++
-    c!.value
     await nextTick()
     expect(computedSpy).toHaveBeenCalledTimes(2)
     expect(watchSpy).toHaveBeenCalledTimes(1)
@@ -265,7 +264,6 @@ describe('reactivity/effect/scope', () => {
     scope.stop()
 
     r.value++
-    c!.value
     await nextTick()
     // should not trigger anymore
     expect(computedSpy).toHaveBeenCalledTimes(2)
index 953765dd1d9c69b1ab283d075238b2534ce192df..678600751a9fcce055f5822a5477439f67d7dcfc 100644 (file)
@@ -6,7 +6,7 @@ import {
   shallowRef as ref,
   toRaw,
 } from '../src/index'
-import { getDepFromReactive } from '../src/reactiveEffect'
+import { getDepFromReactive } from '../src/dep'
 
 describe.skipIf(!global.gc)('reactivity/gc', () => {
   const gc = () => {
diff --git a/packages/reactivity/__tests__/reactiveObject.bench.ts b/packages/reactivity/__tests__/reactiveObject.bench.ts
deleted file mode 100644 (file)
index 7163228..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-import { bench } from 'vitest'
-import { type ComputedRef, computed, reactive } from '../src'
-
-bench('create reactive obj', () => {
-  reactive({ a: 1 })
-})
-
-{
-  let i = 0
-  const r = reactive({ a: 1 })
-  bench('write reactive obj property', () => {
-    r.a = i++
-  })
-}
-
-{
-  const r = reactive({ a: 1 })
-  computed(() => {
-    return r.a * 2
-  })
-  let i = 0
-  bench("write reactive obj, don't read computed (never invoked)", () => {
-    r.a = i++
-  })
-}
-
-{
-  const r = reactive({ a: 1 })
-  const c = computed(() => {
-    return r.a * 2
-  })
-  c.value
-  let i = 0
-  bench("write reactive obj, don't read computed (invoked)", () => {
-    r.a = i++
-  })
-}
-
-{
-  const r = reactive({ a: 1 })
-  const c = computed(() => {
-    return r.a * 2
-  })
-  let i = 0
-  bench('write reactive obj, read computed', () => {
-    r.a = i++
-    c.value
-  })
-}
-
-{
-  const r = reactive({ a: 1 })
-  const computeds = []
-  for (let i = 0, n = 1000; i < n; i++) {
-    const c = computed(() => {
-      return r.a * 2
-    })
-    computeds.push(c)
-  }
-  let i = 0
-  bench("write reactive obj, don't read 1000 computeds (never invoked)", () => {
-    r.a = i++
-  })
-}
-
-{
-  const r = reactive({ a: 1 })
-  const computeds = []
-  for (let i = 0, n = 1000; i < n; i++) {
-    const c = computed(() => {
-      return r.a * 2
-    })
-    c.value
-    computeds.push(c)
-  }
-  let i = 0
-  bench("write reactive obj, don't read 1000 computeds (invoked)", () => {
-    r.a = i++
-  })
-}
-
-{
-  const r = reactive({ a: 1 })
-  const computeds: ComputedRef<number>[] = []
-  for (let i = 0, n = 1000; i < n; i++) {
-    const c = computed(() => {
-      return r.a * 2
-    })
-    computeds.push(c)
-  }
-  let i = 0
-  bench('write reactive obj, read 1000 computeds', () => {
-    r.a = i++
-    computeds.forEach(c => c.value)
-  })
-}
-
-{
-  const reactives: Record<string, number>[] = []
-  for (let i = 0, n = 1000; i < n; i++) {
-    reactives.push(reactive({ a: i }))
-  }
-  const c = computed(() => {
-    let total = 0
-    reactives.forEach(r => (total += r.a))
-    return total
-  })
-  let i = 0
-  const n = reactives.length
-  bench('1000 reactive objs, 1 computed', () => {
-    reactives[i++ % n].a++
-    c.value
-  })
-}
index 66da71a8c9e3bb4c226d166b5df2281d348ef5f9..e86c7fa5b5096304397429871de55b1fc841d4bb 100644 (file)
@@ -409,7 +409,7 @@ describe('reactivity/readonly', () => {
     const eff = effect(() => {
       roArr.includes(2)
     })
-    expect(eff.effect.deps.length).toBe(0)
+    expect(eff.effect.deps).toBeUndefined()
   })
 
   test('readonly should track and trigger if wrapping reactive original (collection)', () => {
index 2b2024d972391505de3da68a64302f85216722ff..ed917dbdd92c3ca16004237854a89d196cd20c04 100644 (file)
@@ -442,4 +442,15 @@ describe('reactivity/ref', () => {
     expect(a.value).toBe(rr)
     expect(a.value).not.toBe(r)
   })
+
+  test('should not trigger when setting the same raw object', () => {
+    const obj = {}
+    const r = ref(obj)
+    const spy = vi.fn()
+    effect(() => spy(r.value))
+    expect(spy).toHaveBeenCalledTimes(1)
+
+    r.value = obj
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
 })
index e9b64d39b36aef655d16734dbb2f662e7fad0840..a5218658a275f5ece8867043453afbdbc8fef88f 100644 (file)
@@ -160,6 +160,7 @@ describe('shallowReactive', () => {
       shallowArray.pop()
       expect(size).toBe(0)
     })
+
     test('should not observe when iterating', () => {
       const shallowArray = shallowReactive<object[]>([])
       const a = {}
index a1b3003a5e7b90795be13649bc04677e9487e20d..ab2ed378129cf9e23485677e7fcc8cd2b809a973 100644 (file)
@@ -11,13 +11,7 @@ import {
   toRaw,
 } from './reactive'
 import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
-import {
-  pauseScheduling,
-  pauseTracking,
-  resetScheduling,
-  resetTracking,
-} from './effect'
-import { ITERATE_KEY, track, trigger } from './reactiveEffect'
+import { ITERATE_KEY, track, trigger } from './dep'
 import {
   hasChanged,
   hasOwn,
@@ -29,6 +23,7 @@ import {
 } from '@vue/shared'
 import { isRef } from './ref'
 import { warn } from './warning'
+import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
 
 const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)
 
@@ -69,11 +64,11 @@ function createArrayInstrumentations() {
   // which leads to infinite loops in some cases (#2137)
   ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
     instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
+      startBatch()
       pauseTracking()
-      pauseScheduling()
       const res = (toRaw(this) as any)[key].apply(this, args)
-      resetScheduling()
       resetTracking()
+      endBatch()
       return res
     }
   })
@@ -133,7 +128,14 @@ class BaseReactiveHandler implements ProxyHandler<Target> {
       }
     }
 
-    const res = Reflect.get(target, key, receiver)
+    const res = Reflect.get(
+      target,
+      key,
+      // if this is a proxy wrapping a ref, return methods using the raw ref
+      // as receiver so that we don't have to call `toRaw` on the ref in all
+      // its class methods
+      isRef(target) ? target : receiver,
+    )
 
     if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
       return res
index 58e69b1cc628dbc167c95582f9c593fcd953a0d3..2636287b6106269462c37d2dc2f740aae3ee3324 100644 (file)
@@ -1,10 +1,5 @@
 import { toRaw, toReactive, toReadonly } from './reactive'
-import {
-  ITERATE_KEY,
-  MAP_KEY_ITERATE_KEY,
-  track,
-  trigger,
-} from './reactiveEffect'
+import { ITERATE_KEY, MAP_KEY_ITERATE_KEY, track, trigger } from './dep'
 import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
 import { capitalize, hasChanged, hasOwn, isMap, toRawType } from '@vue/shared'
 
index a4b74172fcfd98a929ba5eb212db3aade8ad604d..3e0fce6ec0209841344a403edc3627f884bbfd65 100644 (file)
@@ -1,10 +1,18 @@
-import { type DebuggerOptions, ReactiveEffect } from './effect'
-import { type Ref, trackRefValue, triggerRefValue } from './ref'
-import { NOOP, hasChanged, isFunction } from '@vue/shared'
-import { toRaw } from './reactive'
-import type { Dep } from './dep'
-import { DirtyLevels, ReactiveFlags } from './constants'
+import { isFunction } from '@vue/shared'
+import {
+  type DebuggerEvent,
+  type DebuggerOptions,
+  EffectFlags,
+  type Link,
+  type ReactiveEffect,
+  type Subscriber,
+  activeSub,
+  refreshComputed,
+} from './effect'
+import type { Ref } from './ref'
 import { warn } from './warning'
+import { Dep, globalVersion } from './dep'
+import { ReactiveFlags, TrackOpTypes } from './constants'
 
 declare const ComputedRefSymbol: unique symbol
 
@@ -14,7 +22,10 @@ export interface ComputedRef<T = any> extends WritableComputedRef<T> {
 }
 
 export interface WritableComputedRef<T> extends Ref<T> {
-  readonly effect: ReactiveEffect<T>
+  /**
+   * @deprecated computed no longer uses effect
+   */
+  effect: ReactiveEffect
 }
 
 export type ComputedGetter<T> = (oldValue?: T) => T
@@ -25,74 +36,71 @@ export interface WritableComputedOptions<T> {
   set: ComputedSetter<T>
 }
 
-export const COMPUTED_SIDE_EFFECT_WARN =
-  `Computed is still dirty after getter evaluation,` +
-  ` likely because a computed is mutating its own dependency in its getter.` +
-  ` State mutations in computed getters should be avoided. ` +
-  ` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free`
-
-export class ComputedRefImpl<T> {
-  public dep?: Dep = undefined
-
-  private _value!: T
-  public readonly effect: ReactiveEffect<T>
-
-  public readonly __v_isRef = true
-  public readonly [ReactiveFlags.IS_READONLY]: boolean = false
-
-  public _cacheable: boolean
+/**
+ * @internal
+ */
+export class ComputedRefImpl<T = any> implements Subscriber {
+  // A computed is a ref
+  _value: any = undefined
+  readonly dep = new Dep(this)
+  readonly __v_isRef = true;
+  readonly [ReactiveFlags.IS_READONLY]: boolean
+  // A computed is also a subscriber that tracks other deps
+  deps?: Link = undefined
+  depsTail?: Link = undefined
+  // track variaous states
+  flags = EffectFlags.DIRTY
+  // last seen global version
+  globalVersion = globalVersion - 1
+  // for backwards compat
+  effect = this
+
+  // dev only
+  onTrack?: (event: DebuggerEvent) => void
+  // dev only
+  onTrigger?: (event: DebuggerEvent) => void
 
   constructor(
-    getter: ComputedGetter<T>,
-    private readonly _setter: ComputedSetter<T>,
-    isReadonly: boolean,
-    isSSR: boolean,
+    public fn: ComputedGetter<T>,
+    private readonly setter: ComputedSetter<T> | undefined,
+    public isSSR: boolean,
   ) {
-    this.effect = new ReactiveEffect(
-      () => getter(this._value),
-      () =>
-        triggerRefValue(
-          this,
-          this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
-            ? DirtyLevels.MaybeDirty_ComputedSideEffect
-            : DirtyLevels.MaybeDirty,
-        ),
-    )
-    this.effect.computed = this
-    this.effect.active = this._cacheable = !isSSR
-    this[ReactiveFlags.IS_READONLY] = isReadonly
+    this.__v_isReadonly = !setter
   }
 
-  get value() {
-    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
-    const self = toRaw(this)
-    if (
-      (!self._cacheable || self.effect.dirty) &&
-      hasChanged(self._value, (self._value = self.effect.run()!))
-    ) {
-      triggerRefValue(self, DirtyLevels.Dirty)
+  notify() {
+    // avoid infinite self recursion
+    if (activeSub !== this) {
+      this.flags |= EffectFlags.DIRTY
+      this.dep.notify()
+    } else if (__DEV__) {
+      // TODO warn
     }
-    trackRefValue(self)
-    if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
-      __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN)
-      triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
-    }
-    return self._value
-  }
-
-  set value(newValue: T) {
-    this._setter(newValue)
   }
 
-  // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
-  get _dirty() {
-    return this.effect.dirty
+  get value() {
+    const link = __DEV__
+      ? this.dep.track({
+          target: this,
+          type: TrackOpTypes.GET,
+          key: 'value',
+        })
+      : this.dep.track()
+    refreshComputed(this)
+    // sync version after evaluation
+    if (link) {
+      link.version = this.dep.version
+    }
+    return this._value
   }
 
-  set _dirty(v) {
-    this.effect.dirty = v
+  set value(newValue) {
+    if (this.setter) {
+      this.setter(newValue)
+    } else if (__DEV__) {
+      warn('Write operation failed: computed value is readonly')
+    }
   }
-  // #endregion
 }
 
 /**
@@ -142,26 +150,20 @@ export function computed<T>(
   isSSR = false,
 ) {
   let getter: ComputedGetter<T>
-  let setter: ComputedSetter<T>
+  let setter: ComputedSetter<T> | undefined
 
-  const onlyGetter = isFunction(getterOrOptions)
-  if (onlyGetter) {
+  if (isFunction(getterOrOptions)) {
     getter = getterOrOptions
-    setter = __DEV__
-      ? () => {
-          warn('Write operation failed: computed value is readonly')
-        }
-      : NOOP
   } else {
     getter = getterOrOptions.get
     setter = getterOrOptions.set
   }
 
-  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
+  const cRef = new ComputedRefImpl(getter, setter, isSSR)
 
   if (__DEV__ && debugOptions && !isSSR) {
-    cRef.effect.onTrack = debugOptions.onTrack
-    cRef.effect.onTrigger = debugOptions.onTrigger
+    cRef.onTrack = debugOptions.onTrack
+    cRef.onTrigger = debugOptions.onTrigger
   }
 
   return cRef as any
diff --git a/packages/reactivity/src/deferredComputed.ts b/packages/reactivity/src/deferredComputed.ts
deleted file mode 100644 (file)
index 1dbba1f..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-import { computed } from './computed'
-
-/**
- * @deprecated use `computed` instead. See #5912
- */
-export const deferredComputed = computed
index c8e8a130dc9bc199ea201ec0eb206171a7f6afcf..5ba61d3a03fda4c2627f9f91bd0af92a0b6cf8fb 100644 (file)
-import type { ReactiveEffect } from './effect'
+import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
 import type { ComputedRefImpl } from './computed'
+import { type TrackOpTypes, TriggerOpTypes } from './constants'
+import {
+  type DebuggerEventExtraInfo,
+  EffectFlags,
+  type Link,
+  activeSub,
+  endBatch,
+  shouldTrack,
+  startBatch,
+} from './effect'
 
-export type Dep = Map<ReactiveEffect, number> & {
-  cleanup: () => void
-  computed?: ComputedRefImpl<any>
+/**
+ * Incremented every time a reactive change happens
+ * This is used to give computed a fast path to avoid re-compute when nothing
+ * has changed.
+ */
+export let globalVersion = 0
+
+/**
+ * @internal
+ */
+export class Dep {
+  version = 0
+  /**
+   * Link between this dep and the current active effect
+   */
+  activeLink?: Link = undefined
+  /**
+   * Doubly linked list representing the subscribing effects (tail)
+   */
+  subs?: Link = undefined
+
+  constructor(public computed?: ComputedRefImpl) {}
+
+  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
+    if (!activeSub || !shouldTrack) {
+      return
+    }
+
+    let link = this.activeLink
+    if (link === undefined || link.sub !== activeSub) {
+      link = this.activeLink = {
+        dep: this,
+        sub: activeSub,
+        version: this.version,
+        nextDep: undefined,
+        prevDep: undefined,
+        nextSub: undefined,
+        prevSub: undefined,
+        prevActiveLink: undefined,
+      }
+
+      // add the link to the activeEffect as a dep (as tail)
+      if (!activeSub.deps) {
+        activeSub.deps = activeSub.depsTail = link
+      } else {
+        link.prevDep = activeSub.depsTail
+        activeSub.depsTail!.nextDep = link
+        activeSub.depsTail = link
+      }
+
+      if (activeSub.flags & EffectFlags.TRACKING) {
+        addSub(link)
+      }
+    } else if (link.version === -1) {
+      // reused from last run - already a sub, just sync version
+      link.version = this.version
+
+      // If this dep has a next, it means it's not at the tail - move it to the
+      // tail. This ensures the effect's dep list is in the order they are
+      // accessed during evaluation.
+      if (link.nextDep) {
+        const next = link.nextDep
+        next.prevDep = link.prevDep
+        if (link.prevDep) {
+          link.prevDep.nextDep = next
+        }
+
+        link.prevDep = activeSub.depsTail
+        link.nextDep = undefined
+        activeSub.depsTail!.nextDep = link
+        activeSub.depsTail = link
+
+        // this was the head - point to the new head
+        if (activeSub.deps === link) {
+          activeSub.deps = next
+        }
+      }
+    }
+
+    if (__DEV__ && activeSub.onTrack) {
+      activeSub.onTrack(
+        extend(
+          {
+            effect: activeSub,
+          },
+          debugInfo,
+        ),
+      )
+    }
+
+    return link
+  }
+
+  trigger(debugInfo?: DebuggerEventExtraInfo) {
+    this.version++
+    globalVersion++
+    this.notify(debugInfo)
+  }
+
+  notify(debugInfo?: DebuggerEventExtraInfo) {
+    startBatch()
+    try {
+      for (let link = this.subs; link; link = link.prevSub) {
+        if (
+          __DEV__ &&
+          link.sub.onTrigger &&
+          !(link.sub.flags & EffectFlags.NOTIFIED)
+        ) {
+          link.sub.onTrigger(
+            extend(
+              {
+                effect: link.sub,
+              },
+              debugInfo,
+            ),
+          )
+        }
+        link.sub.notify()
+      }
+    } finally {
+      endBatch()
+    }
+  }
+}
+
+function addSub(link: Link) {
+  const computed = link.dep.computed
+  // computed getting its first subscriber
+  // enable tracking + lazily subscribe to all its deps
+  if (computed && !link.dep.subs) {
+    computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
+    for (let l = computed.deps; l; l = l.nextDep) {
+      addSub(l)
+    }
+  }
+
+  const currentTail = link.dep.subs
+  if (currentTail !== link) {
+    link.prevSub = currentTail
+    if (currentTail) currentTail.nextSub = link
+  }
+  link.dep.subs = link
+}
+
+// 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 Maps 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 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 && activeSub) {
+    let depsMap = targetMap.get(target)
+    if (!depsMap) {
+      targetMap.set(target, (depsMap = new Map()))
+    }
+    let dep = depsMap.get(key)
+    if (!dep) {
+      depsMap.set(key, (dep = new Dep()))
+    }
+    if (__DEV__) {
+      dep.track({
+        target,
+        type,
+        key,
+      })
+    } else {
+      dep.track()
+    }
+  }
+}
+
+/**
+ * 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
+    globalVersion++
+    return
+  }
+
+  let deps: Dep[] = []
+  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 {
+    const push = (dep: Dep | undefined) => dep && deps.push(dep)
+
+    // schedule runs for SET | ADD | DELETE
+    if (key !== void 0) {
+      push(depsMap.get(key))
+    }
+
+    // also run for iteration key on ADD | DELETE | Map.SET
+    switch (type) {
+      case TriggerOpTypes.ADD:
+        if (!isArray(target)) {
+          push(depsMap.get(ITERATE_KEY))
+          if (isMap(target)) {
+            push(depsMap.get(MAP_KEY_ITERATE_KEY))
+          }
+        } else if (isIntegerKey(key)) {
+          // new index added to array -> length changes
+          push(depsMap.get('length'))
+        }
+        break
+      case TriggerOpTypes.DELETE:
+        if (!isArray(target)) {
+          push(depsMap.get(ITERATE_KEY))
+          if (isMap(target)) {
+            push(depsMap.get(MAP_KEY_ITERATE_KEY))
+          }
+        }
+        break
+      case TriggerOpTypes.SET:
+        if (isMap(target)) {
+          push(depsMap.get(ITERATE_KEY))
+        }
+        break
+    }
+  }
+
+  startBatch()
+  for (const dep of deps) {
+    if (__DEV__) {
+      dep.trigger({
+        target,
+        type,
+        key,
+        newValue,
+        oldValue,
+        oldTarget,
+      })
+    } else {
+      dep.trigger()
+    }
+  }
+  endBatch()
 }
 
-export const createDep = (
-  cleanup: () => void,
-  computed?: ComputedRefImpl<any>,
-): Dep => {
-  const dep = new Map() as Dep
-  dep.cleanup = cleanup
-  dep.computed = computed
-  return dep
+/**
+ * Test only
+ */
+export function getDepFromReactive(object: any, key: string | number | symbol) {
+  return targetMap.get(object)?.get(key)
 }
index ca90544c0deab7ddd29a441137b2c6656d57fb32..5a4d05268dc6ff530a5524e9255f050f38035e1f 100644 (file)
@@ -1,17 +1,14 @@
-import { NOOP, extend } from '@vue/shared'
+import { extend, hasChanged } from '@vue/shared'
 import type { ComputedRefImpl } from './computed'
-import {
-  DirtyLevels,
-  type TrackOpTypes,
-  type TriggerOpTypes,
-} from './constants'
-import type { Dep } from './dep'
-import { type EffectScope, recordEffectScope } from './effectScope'
+import type { TrackOpTypes, TriggerOpTypes } from './constants'
+import { type Dep, globalVersion } from './dep'
+import { recordEffectScope } from './effectScope'
+import { warn } from './warning'
 
 export type EffectScheduler = (...args: any[]) => any
 
 export type DebuggerEvent = {
-  effect: ReactiveEffect
+  effect: Subscriber
 } & DebuggerEventExtraInfo
 
 export type DebuggerEventExtraInfo = {
@@ -23,156 +20,398 @@ export type DebuggerEventExtraInfo = {
   oldTarget?: Map<any, any> | Set<any>
 }
 
-export let activeEffect: ReactiveEffect | undefined
+export interface DebuggerOptions {
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
+}
 
-export class ReactiveEffect<T = any> {
-  active = true
-  deps: Dep[] = []
+export interface ReactiveEffectOptions extends DebuggerOptions {
+  scheduler?: EffectScheduler
+  allowRecurse?: boolean
+  onStop?: () => void
+}
 
+export interface ReactiveEffectRunner<T = any> {
+  (): T
+  effect: ReactiveEffect
+}
+
+export let activeSub: Subscriber | undefined
+
+export enum EffectFlags {
+  ACTIVE = 1 << 0,
+  RUNNING = 1 << 1,
+  TRACKING = 1 << 2,
+  NOTIFIED = 1 << 3,
+  DIRTY = 1 << 4,
+  ALLOW_RECURSE = 1 << 5,
+  NO_BATCH = 1 << 6,
+}
+
+/**
+ * Subscriber is a type that tracks (or subscribes to) a list of deps.
+ */
+export interface Subscriber extends DebuggerOptions {
   /**
-   * Can be attached after creation
+   * Head of the doubly linked list representing the deps
    * @internal
    */
-  computed?: ComputedRefImpl<T>
+  deps?: Link
   /**
+   * Tail of the same list
    * @internal
    */
-  allowRecurse?: boolean
+  depsTail?: Link
+  /**
+   * @internal
+   */
+  flags: EffectFlags
+  /**
+   * @internal
+   */
+  notify(): void
+}
 
-  onStop?: () => void
-  // dev only
-  onTrack?: (event: DebuggerEvent) => void
-  // dev only
-  onTrigger?: (event: DebuggerEvent) => void
+/**
+ * Represents a link between a source (Dep) and a subscriber (Effect or Computed).
+ * Deps and subs have a many-to-many relationship - each link between a
+ * dep and a sub is represented by a Link instance.
+ *
+ * A Link is also a node in two doubly-linked lists - one for the associated
+ * sub to track all its deps, and one for the associated dep to track all its
+ * subs.
+ *
+ * @internal
+ */
+export interface Link {
+  dep: Dep
+  sub: Subscriber
+
+  /**
+   * - Before each effect run, all previous dep links' version are reset to -1
+   * - During the run, a link's version is synced with the source dep on access
+   * - After the run, links with version -1 (that were never used) are cleaned
+   *   up
+   */
+  version: number
 
+  /**
+   * Pointers for doubly-linked lists
+   */
+  nextDep?: Link
+  prevDep?: Link
+
+  nextSub?: Link
+  prevSub?: Link
+
+  prevActiveLink?: Link
+}
+
+export class ReactiveEffect<T = any>
+  implements Subscriber, ReactiveEffectOptions
+{
   /**
    * @internal
    */
-  _dirtyLevel = DirtyLevels.Dirty
+  deps?: Link = undefined
   /**
    * @internal
    */
-  _trackId = 0
+  depsTail?: Link = undefined
   /**
    * @internal
    */
-  _runnings = 0
+  flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
   /**
    * @internal
    */
-  _shouldSchedule = false
+  nextEffect?: ReactiveEffect = undefined
   /**
    * @internal
    */
-  _depsLength = 0
+  allowRecurse?: boolean
 
-  constructor(
-    public fn: () => T,
-    public trigger: () => void,
-    public scheduler?: EffectScheduler,
-    scope?: EffectScope,
-  ) {
-    recordEffectScope(this, scope)
-  }
+  scheduler?: EffectScheduler = undefined
+  onStop?: () => void
+  onTrack?: (event: DebuggerEvent) => void
+  onTrigger?: (event: DebuggerEvent) => void
 
-  public get dirty() {
-    if (
-      this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
-      this._dirtyLevel === DirtyLevels.MaybeDirty
-    ) {
-      this._dirtyLevel = DirtyLevels.QueryingDirty
-      pauseTracking()
-      for (let i = 0; i < this._depsLength; i++) {
-        const dep = this.deps[i]
-        if (dep.computed) {
-          triggerComputed(dep.computed)
-          if (this._dirtyLevel >= DirtyLevels.Dirty) {
-            break
-          }
-        }
-      }
-      if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
-        this._dirtyLevel = DirtyLevels.NotDirty
-      }
-      resetTracking()
-    }
-    return this._dirtyLevel >= DirtyLevels.Dirty
+  constructor(public fn: () => T) {
+    recordEffectScope(this)
   }
 
-  public set dirty(v) {
-    this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
+  /**
+   * @internal
+   */
+  notify() {
+    if (this.flags & EffectFlags.RUNNING && !this.allowRecurse) {
+      return
+    }
+    if (this.flags & EffectFlags.NO_BATCH) {
+      return this.trigger()
+    }
+    if (!(this.flags & EffectFlags.NOTIFIED)) {
+      this.flags |= EffectFlags.NOTIFIED
+      this.nextEffect = batchedEffect
+      batchedEffect = this
+    }
   }
 
   run() {
-    this._dirtyLevel = DirtyLevels.NotDirty
-    if (!this.active) {
+    // TODO cleanupEffect
+
+    if (!(this.flags & EffectFlags.ACTIVE)) {
+      // stopped during cleanup
       return this.fn()
     }
-    let lastShouldTrack = shouldTrack
-    let lastEffect = activeEffect
+
+    this.flags |= EffectFlags.RUNNING
+    prepareDeps(this)
+    const prevEffect = activeSub
+    const prevShouldTrack = shouldTrack
+    activeSub = this
+    shouldTrack = true
+
     try {
-      shouldTrack = true
-      activeEffect = this
-      this._runnings++
-      preCleanupEffect(this)
       return this.fn()
     } finally {
-      postCleanupEffect(this)
-      this._runnings--
-      activeEffect = lastEffect
-      shouldTrack = lastShouldTrack
+      if (__DEV__ && activeSub !== this) {
+        warn(
+          'Active effect was not restored correctly - ' +
+            'this is likely a Vue internal bug.',
+        )
+      }
+      cleanupDeps(this)
+      activeSub = prevEffect
+      shouldTrack = prevShouldTrack
+      this.flags &= ~EffectFlags.RUNNING
     }
   }
 
   stop() {
-    if (this.active) {
-      preCleanupEffect(this)
-      postCleanupEffect(this)
-      this.onStop?.()
-      this.active = false
+    if (this.flags & EffectFlags.ACTIVE) {
+      for (let link = this.deps; link; link = link.nextDep) {
+        removeSub(link)
+      }
+      this.deps = this.depsTail = undefined
+      this.onStop && this.onStop()
+      this.flags &= ~EffectFlags.ACTIVE
+    }
+  }
+
+  trigger() {
+    if (this.scheduler) {
+      this.scheduler()
+    } else {
+      this.runIfDirty()
+    }
+  }
+
+  /**
+   * @internal
+   */
+  runIfDirty() {
+    if (isDirty(this)) {
+      this.run()
+    }
+  }
+
+  get dirty() {
+    return isDirty(this)
+  }
+}
+
+let batchDepth = 0
+let batchedEffect: ReactiveEffect | undefined
+
+/**
+ * @internal
+ */
+export function startBatch() {
+  batchDepth++
+}
+
+/**
+ * Run batched effects when all batches have ended
+ * @internal
+ */
+export function endBatch() {
+  if (batchDepth > 1) {
+    batchDepth--
+    return
+  }
+
+  let error: unknown
+  while (batchedEffect) {
+    let e: ReactiveEffect | undefined = batchedEffect
+    batchedEffect = undefined
+    while (e) {
+      const next: ReactiveEffect | undefined = e.nextEffect
+      e.nextEffect = undefined
+      e.flags &= ~EffectFlags.NOTIFIED
+      if (e.flags & EffectFlags.ACTIVE) {
+        try {
+          e.trigger()
+        } catch (err) {
+          if (!error) error = err
+        }
+      }
+      e = next
     }
   }
+
+  batchDepth--
+  if (error) throw error
 }
 
-function triggerComputed(computed: ComputedRefImpl<any>) {
-  return computed.value
+function prepareDeps(sub: Subscriber) {
+  // Prepare deps for tracking, starting from the head
+  for (let link = sub.deps; link; link = link.nextDep) {
+    // set all previous deps' (if any) version to -1 so that we can track
+    // which ones are unused after the run
+    link.version = -1
+    // store previous active sub if link was being used in another context
+    link.prevActiveLink = link.dep.activeLink
+    link.dep.activeLink = link
+  }
 }
 
-function preCleanupEffect(effect: ReactiveEffect) {
-  effect._trackId++
-  effect._depsLength = 0
+function cleanupDeps(sub: Subscriber) {
+  // Cleanup unsued deps
+  let head
+  let tail = sub.depsTail
+  for (let link = tail; link; link = link.prevDep) {
+    if (link.version === -1) {
+      if (link === tail) tail = link.prevDep
+      // unused - remove it from the dep's subscribing effect list
+      removeSub(link)
+      // also remove it from this effect's dep list
+      removeDep(link)
+    } else {
+      // The new head is the last node seen which wasn't removed
+      // from the doubly-linked list
+      head = link
+    }
+
+    // restore previous active link if any
+    link.dep.activeLink = link.prevActiveLink
+    link.prevActiveLink = undefined
+  }
+  // set the new head & tail
+  sub.deps = head
+  sub.depsTail = tail
 }
 
-function postCleanupEffect(effect: ReactiveEffect) {
-  if (effect.deps.length > effect._depsLength) {
-    for (let i = effect._depsLength; i < effect.deps.length; i++) {
-      cleanupDepEffect(effect.deps[i], effect)
+function isDirty(sub: Subscriber): boolean {
+  for (let link = sub.deps; link; link = link.nextDep) {
+    if (
+      link.dep.version !== link.version ||
+      (link.dep.computed && refreshComputed(link.dep.computed) === false) ||
+      link.dep.version !== link.version
+    ) {
+      return true
     }
-    effect.deps.length = effect._depsLength
   }
+  // @ts-expect-error only for backwards compatibility where libs manually set
+  // this flag - e.g. Pinia's testing module
+  if (sub._dirty) {
+    return true
+  }
+  return false
 }
 
-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()
+/**
+ * Returning false indicates the refresh failed
+ * @internal
+ */
+export function refreshComputed(computed: ComputedRefImpl) {
+  if (computed.flags & EffectFlags.RUNNING) {
+    return false
+  }
+  if (
+    computed.flags & EffectFlags.TRACKING &&
+    !(computed.flags & EffectFlags.DIRTY)
+  ) {
+    return
+  }
+  computed.flags &= ~EffectFlags.DIRTY
+
+  // Global version fast path when no reactive changes has happened since
+  // last refresh.
+  if (computed.globalVersion === globalVersion) {
+    return
+  }
+  computed.globalVersion = globalVersion
+
+  const dep = computed.dep
+  computed.flags |= EffectFlags.RUNNING
+  // In SSR there will be no render effect, so the computed has no subscriber
+  // and therefore tracks no deps, thus we cannot rely on the dirty check.
+  // Instead, computed always re-evaluate and relies on the globalVersion
+  // fast path above for caching.
+  if (dep.version > 0 && !computed.isSSR && !isDirty(computed)) {
+    computed.flags &= ~EffectFlags.RUNNING
+    return
+  }
+
+  const prevSub = activeSub
+  const prevShouldTrack = shouldTrack
+  activeSub = computed
+  shouldTrack = true
+
+  try {
+    prepareDeps(computed)
+    const value = computed.fn()
+    if (dep.version === 0 || hasChanged(value, computed._value)) {
+      computed._value = value
+      dep.version++
     }
+  } catch (err) {
+    dep.version++
   }
+
+  activeSub = prevSub
+  shouldTrack = prevShouldTrack
+  cleanupDeps(computed)
+  computed.flags &= ~EffectFlags.RUNNING
 }
 
-export interface DebuggerOptions {
-  onTrack?: (event: DebuggerEvent) => void
-  onTrigger?: (event: DebuggerEvent) => void
+function removeSub(link: Link) {
+  const { dep, prevSub, nextSub } = link
+  if (prevSub) {
+    prevSub.nextSub = nextSub
+    link.prevSub = undefined
+  }
+  if (nextSub) {
+    nextSub.prevSub = prevSub
+    link.nextSub = undefined
+  }
+  if (dep.subs === link) {
+    // was previous tail, point new tail to prev
+    dep.subs = prevSub
+  }
+
+  if (!dep.subs && dep.computed) {
+    // last subscriber removed
+    // if computed, unsubscribe it from all its deps so this computed and its
+    // value can be GCed
+    dep.computed.flags &= ~EffectFlags.TRACKING
+    for (let l = dep.computed.deps; l; l = l.nextDep) {
+      removeSub(l)
+    }
+  }
 }
 
-export interface ReactiveEffectOptions extends DebuggerOptions {
-  lazy?: boolean
-  scheduler?: EffectScheduler
-  scope?: EffectScope
-  allowRecurse?: boolean
-  onStop?: () => void
+function removeDep(link: Link) {
+  const { prevDep, nextDep } = link
+  if (prevDep) {
+    prevDep.nextDep = nextDep
+    link.prevDep = undefined
+  }
+  if (nextDep) {
+    nextDep.prevDep = prevDep
+    link.nextDep = undefined
+  }
 }
 
 export interface ReactiveEffectRunner<T = any> {
@@ -180,38 +419,26 @@ export interface ReactiveEffectRunner<T = any> {
   effect: ReactiveEffect
 }
 
-/**
- * Registers the given function to track reactive updates.
- *
- * The given function will be run once immediately. Every time any reactive
- * property that's accessed within it gets updated, the function will run again.
- *
- * @param fn - The function that will track reactive updates.
- * @param options - Allows to control the effect's behaviour.
- * @returns A runner that can be used to control the effect after creation.
- */
 export function effect<T = any>(
   fn: () => T,
   options?: ReactiveEffectOptions,
-): ReactiveEffectRunner {
+): ReactiveEffectRunner<T> {
   if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
     fn = (fn as ReactiveEffectRunner).effect.fn
   }
 
-  const _effect = new ReactiveEffect(fn, NOOP, () => {
-    if (_effect.dirty) {
-      _effect.run()
-    }
-  })
+  const e = new ReactiveEffect(fn)
   if (options) {
-    extend(_effect, options)
-    if (options.scope) recordEffectScope(_effect, options.scope)
+    extend(e, options)
   }
-  if (!options || !options.lazy) {
-    _effect.run()
+  try {
+    e.run()
+  } catch (err) {
+    e.stop()
+    throw err
   }
-  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
-  runner.effect = _effect
+  const runner = e.run.bind(e) as ReactiveEffectRunner
+  runner.effect = e
   return runner
 }
 
@@ -224,9 +451,10 @@ export function stop(runner: ReactiveEffectRunner) {
   runner.effect.stop()
 }
 
+/**
+ * @internal
+ */
 export let shouldTrack = true
-export let pauseScheduleStack = 0
-
 const trackStack: boolean[] = []
 
 /**
@@ -252,76 +480,3 @@ export function resetTracking() {
   const last = trackStack.pop()
   shouldTrack = last === undefined ? true : last
 }
-
-export function pauseScheduling() {
-  pauseScheduleStack++
-}
-
-export function resetScheduling() {
-  pauseScheduleStack--
-  while (!pauseScheduleStack && queueEffectSchedulers.length) {
-    queueEffectSchedulers.shift()!()
-  }
-}
-
-export function trackEffect(
-  effect: ReactiveEffect,
-  dep: Dep,
-  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
-) {
-  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__) {
-      effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
-    }
-  }
-}
-
-const queueEffectSchedulers: EffectScheduler[] = []
-
-export function triggerEffects(
-  dep: Dep,
-  dirtyLevel: DirtyLevels,
-  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
-) {
-  pauseScheduling()
-  for (const effect of dep.keys()) {
-    // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
-    let tracking: boolean | undefined
-    if (
-      effect._dirtyLevel < dirtyLevel &&
-      (tracking ??= dep.get(effect) === effect._trackId)
-    ) {
-      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
-      effect._dirtyLevel = dirtyLevel
-    }
-    if (
-      effect._shouldSchedule &&
-      (tracking ??= dep.get(effect) === effect._trackId)
-    ) {
-      if (__DEV__) {
-        effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
-      }
-      effect.trigger()
-      if (
-        (!effect._runnings || effect.allowRecurse) &&
-        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
-      ) {
-        effect._shouldSchedule = false
-        if (effect.scheduler) {
-          queueEffectSchedulers.push(effect.scheduler)
-        }
-      }
-    }
-  }
-  resetScheduling()
-}
index 1c80fbc752b733290296c05fefb7a7c8aed29351..40bdf7b1b0485f0703dbfa9d96f8c4d6bc4ecd5e 100644 (file)
@@ -44,16 +44,14 @@ export {
   type ComputedGetter,
   type ComputedSetter,
 } from './computed'
-export { deferredComputed } from './deferredComputed'
 export {
   effect,
   stop,
   enableTracking,
   pauseTracking,
   resetTracking,
-  pauseScheduling,
-  resetScheduling,
   ReactiveEffect,
+  EffectFlags,
   type ReactiveEffectRunner,
   type ReactiveEffectOptions,
   type EffectScheduler,
@@ -61,7 +59,7 @@ export {
   type DebuggerEvent,
   type DebuggerEventExtraInfo,
 } from './effect'
-export { trigger, track, ITERATE_KEY } from './reactiveEffect'
+export { trigger, track, ITERATE_KEY } from './dep'
 export {
   effectScope,
   EffectScope,
diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts
deleted file mode 100644 (file)
index 6bf0e75..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
-import { DirtyLevels, type TrackOpTypes, TriggerOpTypes } from './constants'
-import { type Dep, createDep } 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 Maps 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 1b9d60ef06b62a2b78c19e7f02b71e203b932c0c..bfde3b787e3a0962e2b47572c0f2ee91eb3af825 100644 (file)
@@ -1,11 +1,3 @@
-import type { ComputedRef } from './computed'
-import {
-  activeEffect,
-  shouldTrack,
-  trackEffect,
-  triggerEffects,
-} from './effect'
-import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants'
 import {
   type IfAny,
   hasChanged,
@@ -13,7 +5,9 @@ import {
   isFunction,
   isObject,
 } from '@vue/shared'
+import { Dep, getDepFromReactive } from './dep'
 import {
+  type ShallowReactiveMarker,
   isProxy,
   isReactive,
   isReadonly,
@@ -21,10 +15,8 @@ import {
   toRaw,
   toReactive,
 } from './reactive'
-import type { ShallowReactiveMarker } from './reactive'
-import { type Dep, createDep } from './dep'
-import { ComputedRefImpl } from './computed'
-import { getDepFromReactive } from './reactiveEffect'
+import type { ComputedRef } from './computed'
+import { TrackOpTypes, TriggerOpTypes } from './constants'
 
 declare const RefSymbol: unique symbol
 export declare const RawSymbol: unique symbol
@@ -39,54 +31,6 @@ export interface Ref<T = any> {
   [RefSymbol]: true
 }
 
-type RefBase<T> = {
-  dep?: Dep
-  value: T
-}
-
-export function trackRefValue(ref: RefBase<any>) {
-  if (shouldTrack && activeEffect) {
-    ref = toRaw(ref)
-    trackEffect(
-      activeEffect,
-      (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>,
-  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
-  newVal?: any,
-) {
-  ref = toRaw(ref)
-  const dep = ref.dep
-  if (dep) {
-    triggerEffects(
-      dep,
-      dirtyLevel,
-      __DEV__
-        ? {
-            target: ref,
-            type: TriggerOpTypes.SET,
-            key: 'value',
-            newValue: newVal,
-          }
-        : void 0,
-    )
-  }
-}
-
 /**
  * Checks if a value is a ref object.
  *
@@ -95,7 +39,7 @@ export function triggerRefValue(
  */
 export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
 export function isRef(r: any): r is Ref {
-  return !!(r && r.__v_isRef === true)
+  return r ? r.__v_isRef === true : false
 }
 
 /**
@@ -151,11 +95,15 @@ function createRef(rawValue: unknown, shallow: boolean) {
   return new RefImpl(rawValue, shallow)
 }
 
-class RefImpl<T> {
-  private _value: T
+/**
+ * @internal
+ */
+class RefImpl<T = any> {
+  _value: T
   private _rawValue: T
 
-  public dep?: Dep = undefined
+  dep: Dep = new Dep()
+
   public readonly __v_isRef = true
 
   constructor(
@@ -167,18 +115,37 @@ class RefImpl<T> {
   }
 
   get value() {
-    trackRefValue(this)
+    if (__DEV__) {
+      this.dep.track({
+        target: this,
+        type: TrackOpTypes.GET,
+        key: 'value',
+      })
+    } else {
+      this.dep.track()
+    }
     return this._value
   }
 
-  set value(newVal) {
+  set value(newValue) {
+    const oldValue = this._rawValue
     const useDirectValue =
-      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
-    newVal = useDirectValue ? newVal : toRaw(newVal)
-    if (hasChanged(newVal, this._rawValue)) {
-      this._rawValue = newVal
-      this._value = useDirectValue ? newVal : toReactive(newVal)
-      triggerRefValue(this, DirtyLevels.Dirty, newVal)
+      this.__v_isShallow || isShallow(newValue) || isReadonly(newValue)
+    newValue = useDirectValue ? newValue : toRaw(newValue)
+    if (hasChanged(newValue, oldValue)) {
+      this._rawValue = newValue
+      this._value = useDirectValue ? newValue : toReactive(newValue)
+      if (__DEV__) {
+        this.dep.trigger({
+          target: this,
+          type: TriggerOpTypes.SET,
+          key: 'value',
+          newValue,
+          oldValue,
+        })
+      } else {
+        this.dep.trigger()
+      }
     }
   }
 }
@@ -209,7 +176,16 @@ class RefImpl<T> {
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
  */
 export function triggerRef(ref: Ref) {
-  triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0)
+  if (__DEV__) {
+    ;(ref as unknown as RefImpl).dep.trigger({
+      target: ref,
+      type: TriggerOpTypes.SET,
+      key: 'value',
+      newValue: (ref as unknown as RefImpl)._value,
+    })
+  } else {
+    ;(ref as unknown as RefImpl).dep.trigger()
+  }
 }
 
 export type MaybeRef<T = any> = T | Ref<T>
@@ -295,7 +271,7 @@ export type CustomRefFactory<T> = (
 }
 
 class CustomRefImpl<T> {
-  public dep?: Dep = undefined
+  public dep: Dep
 
   private readonly _get: ReturnType<CustomRefFactory<T>>['get']
   private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@@ -303,10 +279,8 @@ class CustomRefImpl<T> {
   public readonly __v_isRef = true
 
   constructor(factory: CustomRefFactory<T>) {
-    const { get, set } = factory(
-      () => trackRefValue(this),
-      () => triggerRefValue(this),
-    )
+    const dep = (this.dep = new Dep())
+    const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
     this._get = get
     this._set = set
   }
index 04e9c1c86dbba0b9b3ea641569d2de921c5e2053..30c8951f40562f20292700b37225119d401f79d9 100644 (file)
@@ -1,6 +1,5 @@
 import {
   type ComponentInternalInstance,
-  type ComputedRef,
   type SetupContext,
   Suspense,
   computed,
@@ -26,6 +25,8 @@ import {
   withAsyncContext,
   withDefaults,
 } from '../src/apiSetupHelpers'
+import type { ComputedRefImpl } from '../../reactivity/src/computed'
+import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
 
 describe('SFC <script setup> helpers', () => {
   test('should warn runtime usage', () => {
@@ -426,7 +427,8 @@ describe('SFC <script setup> helpers', () => {
         resolve = r
       })
 
-      let c: ComputedRef
+      let c: ComputedRefImpl
+      let e: ReactiveEffectRunner
 
       const Comp = defineComponent({
         async setup() {
@@ -435,10 +437,11 @@ describe('SFC <script setup> helpers', () => {
           __temp = await __temp
           __restore()
 
-          c = computed(() => {})
+          c = computed(() => {}) as unknown as ComputedRefImpl
+          e = effect(() => c.value)
           // register the lifecycle after an await statement
           onMounted(resolve)
-          return () => ''
+          return () => c.value
         },
       })
 
@@ -447,10 +450,12 @@ describe('SFC <script setup> helpers', () => {
       app.mount(root)
 
       await ready
-      expect(c!.effect.active).toBe(true)
+      expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
+      expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
 
       app.unmount()
-      expect(c!.effect.active).toBe(false)
+      expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
+      expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
     })
   })
 })
index fe299edbb63423e615684ef4f7ec97e070fe2881..991419d8ef664ca1075907c1a85981b5cfd370b0 100644 (file)
@@ -23,6 +23,7 @@ import {
 } from '@vue/runtime-test'
 import {
   type DebuggerEvent,
+  EffectFlags,
   ITERATE_KEY,
   type Ref,
   type ShallowRef,
@@ -1185,7 +1186,7 @@ describe('api: watch', () => {
     await nextTick()
     await nextTick()
 
-    expect(instance!.scope.effects[0].active).toBe(false)
+    expect(instance!.scope.effects[0].flags & EffectFlags.ACTIVE).toBeFalsy()
   })
 
   test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@@ -1474,4 +1475,46 @@ describe('api: watch', () => {
     unwatch!()
     expect(scope.effects.length).toBe(0)
   })
+
+  // simplified case of VueUse syncRef
+  test('sync watcher should not be batched', () => {
+    const a = ref(0)
+    const b = ref(0)
+    let pauseB = false
+    watch(
+      a,
+      () => {
+        pauseB = true
+        b.value = a.value + 1
+        pauseB = false
+      },
+      { flush: 'sync' },
+    )
+    watch(
+      b,
+      () => {
+        if (!pauseB) {
+          throw new Error('should not be called')
+        }
+      },
+      { flush: 'sync' },
+    )
+
+    a.value = 1
+    expect(b.value).toBe(2)
+  })
+
+  test('watchEffect should not fire on computed deps that did not change', async () => {
+    const a = ref(0)
+    const c = computed(() => a.value % 2)
+    const spy = vi.fn()
+    watchEffect(() => {
+      spy()
+      c.value
+    })
+    expect(spy).toHaveBeenCalledTimes(1)
+    a.value += 2
+    await nextTick()
+    expect(spy).toHaveBeenCalledTimes(1)
+  })
 })
index 4c6e4bad11b6ff2d8294bb434a1b4f7448257fbe..452426c132419d51a958a5f9addd17648611e557 100644 (file)
@@ -187,7 +187,6 @@ 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 bc10547824e2403c70832d1f63b56e26454a905c..556688ebf4bd948824f5ea7d18303e36ea7aaa33 100644 (file)
@@ -1,6 +1,7 @@
 import {
   type ComputedRef,
   type DebuggerOptions,
+  EffectFlags,
   type EffectScheduler,
   ReactiveEffect,
   ReactiveFlags,
@@ -337,8 +338,11 @@ function doWatch(
   let oldValue: any = isMultiSource
     ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
     : INITIAL_WATCHER_VALUE
-  const job: SchedulerJob = () => {
-    if (!effect.active || !effect.dirty) {
+  const job: SchedulerJob = (immediateFirstRun?: boolean) => {
+    if (
+      !(effect.flags & EffectFlags.ACTIVE) ||
+      (!effect.dirty && !immediateFirstRun)
+    ) {
       return
     }
     if (cb) {
@@ -380,8 +384,11 @@ function doWatch(
   // it is allowed to self-trigger (#1727)
   job.allowRecurse = !!cb
 
+  const effect = new ReactiveEffect(getter)
+
   let scheduler: EffectScheduler
   if (flush === 'sync') {
+    effect.flags |= EffectFlags.NO_BATCH
     scheduler = job as any // the scheduler function gets called directly
   } else if (flush === 'post') {
     scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
@@ -391,8 +398,7 @@ function doWatch(
     if (instance) job.id = instance.uid
     scheduler = () => queueJob(job)
   }
-
-  const effect = new ReactiveEffect(getter, NOOP, scheduler)
+  effect.scheduler = scheduler
 
   const scope = getCurrentScope()
   const unwatch = () => {
@@ -410,7 +416,7 @@ function doWatch(
   // initial run
   if (cb) {
     if (immediate) {
-      job()
+      job(true)
     } else {
       oldValue = effect.run()
     }
index ed1f8efee52d4acb46f4122f987991aed9f7faea..5751ffb1806b747e1c20332b160d3503a4ee0e70 100644 (file)
@@ -286,9 +286,13 @@ export interface ComponentInternalInstance {
    */
   effect: ReactiveEffect
   /**
-   * Bound effect runner to be passed to schedulers
+   * Force update render effect
    */
-  update: SchedulerJob
+  update: () => void
+  /**
+   * Render effect job to be passed to scheduler (checks if dirty)
+   */
+  job: SchedulerJob
   /**
    * The render function that returns vdom tree.
    * @internal
@@ -552,6 +556,7 @@ export function createComponentInstance(
     subTree: null!, // will be set synchronously right after creation
     effect: null!,
     update: null!, // will be set synchronously right after creation
+    job: null!,
     scope: new EffectScope(true /* detached */),
     render: null,
     proxy: null,
index 5b2b4f2303d52650efdd18d5189948e7930caae2..e3f0d24af0c9980be0b8855fbedbaac51a66aa45 100644 (file)
@@ -276,7 +276,6 @@ export const publicPropertiesMap: PublicPropertiesMap =
     $forceUpdate: i =>
       i.f ||
       (i.f = () => {
-        i.effect.dirty = true
         queueJob(i.update)
       }),
     $nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
index 20da25848b61aeb5b076a2257ba6dc60a69d98dd..0a558bc30130e4c4603b9c60841af78143606e93 100644 (file)
@@ -245,8 +245,7 @@ const BaseTransitionImpl: ComponentOptions = {
             state.isLeaving = false
             // #6835
             // it also needs to be updated when active is undefined
-            if (instance.update.active !== false) {
-              instance.effect.dirty = true
+            if (instance.job.active !== false) {
               instance.update()
             }
           }
index abd3a329922677c81763a5c3147298395f7397bf..29ac483e99297f5515fc2323dd2fa18cf1acbdad 100644 (file)
@@ -38,7 +38,8 @@ export function initCustomFormatter() {
           {},
           ['span', vueStyle, genRefFlag(obj)],
           '<',
-          formatValue(obj.value),
+          // avoid debugger accessing value affecting behavior
+          formatValue('_value' in obj ? obj._value : obj),
           `>`,
         ]
       } else if (isReactive(obj)) {
index b5904a4fc5baf40cbbeb95d57f1a8d3b8c6b93f7..08f861f6b13a1ffbaa7232a44363aa74b869d5ae 100644 (file)
@@ -93,7 +93,6 @@ 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
   })
@@ -138,7 +137,6 @@ 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 c7dfbf45dd220e6c8c82e2a4c8b63e9e8686b555..a437771d34a8b0b2bc8b622eaf697cba5c760b3e 100644 (file)
@@ -1289,7 +1289,6 @@ 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 {
@@ -1574,19 +1573,15 @@ 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 = () => {
-      if (effect.dirty) {
-        effect.run()
-      }
-    })
-    update.id = instance.uid
+    instance.scope.on()
+    const effect = (instance.effect = new ReactiveEffect(componentUpdateFn))
+    instance.scope.off()
+
+    const update = (instance.update = effect.run.bind(effect))
+    const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
+    job.id = instance.uid
+    effect.scheduler = () => queueJob(job)
+
     // allowRecurse
     // #1801, #2043 component render effects should allow recursive updates
     toggleRecurse(instance, true)
@@ -1598,7 +1593,7 @@ function baseCreateRenderer(
       effect.onTrigger = instance.rtg
         ? e => invokeArrayFns(instance.rtg!, e)
         : void 0
-      update.ownerInstance = instance
+      job.ownerInstance = instance
     }
 
     update()
@@ -2265,7 +2260,7 @@ function baseCreateRenderer(
       unregisterHMR(instance)
     }
 
-    const { bum, scope, update, subTree, um } = instance
+    const { bum, scope, job, subTree, um } = instance
 
     // beforeUnmount hook
     if (bum) {
@@ -2282,11 +2277,11 @@ function baseCreateRenderer(
     // stop effects in component scope
     scope.stop()
 
-    // update may be null if a component is unmounted before its async
+    // job may be null if a component is unmounted before its async
     // setup has resolved.
-    if (update) {
+    if (job) {
       // so that scheduler will no longer invoke it
-      update.active = false
+      job.active = false
       unmount(subTree, instance, parentSuspense, doRemove)
     }
     // unmounted hook
@@ -2421,10 +2416,10 @@ function resolveChildrenNamespace(
 }
 
 function toggleRecurse(
-  { effect, update }: ComponentInternalInstance,
+  { effect, job }: ComponentInternalInstance,
   allowed: boolean,
 ) {
-  effect.allowRecurse = update.allowRecurse = allowed
+  effect.allowRecurse = job.allowRecurse = allowed
 }
 
 export function needTransition(
index b8d1445a183db551226294d68e65f7f0a1f3350d..866f4de0fd46ce697493025227b6c3cd37ff549d 100644 (file)
@@ -6,7 +6,6 @@ export interface SchedulerJob extends Function {
   id?: number
   pre?: boolean
   active?: boolean
-  computed?: boolean
   /**
    * Indicates whether the effect is allowed to recursively trigger itself
    * when managed by the scheduler.
index 646fa83731c79f7283be82952312381b4b9111ad..bf0800ff2538acbeba3170a5b66a6dc175ed078c 100644 (file)
@@ -1,4 +1,4 @@
-import { computed, createSSRApp, defineComponent, h, reactive } from 'vue'
+import { computed, createSSRApp, defineComponent, h, reactive, ref } from 'vue'
 import { renderToString } from '../src/renderToString'
 
 // #5208 reported memory leak of keeping computed alive during SSR
@@ -45,3 +45,23 @@ test('computed reactivity during SSR', async () => {
   // during the render phase
   expect(getterSpy).toHaveBeenCalledTimes(2)
 })
+
+// although we technically shouldn't allow state mutation during render,
+// it does sometimes happen
+test('computed mutation during render', async () => {
+  const App = defineComponent(async () => {
+    const n = ref(0)
+    const m = computed(() => n.value + 1)
+
+    m.value // force non-dirty
+
+    return () => {
+      n.value++
+      return h('div', null, `value: ${m.value}`)
+    }
+  })
+
+  const app = createSSRApp(App)
+  const html = await renderToString(app)
+  expect(html).toMatch('value: 2')
+})
index 3029df6e54058ec1414b629cbd96e856123b18dd..0e6310f42a055f6b9ca0a0edbdfd605f219ea78c 100644 (file)
@@ -143,15 +143,6 @@ function renderComponentSubTree(
       comp.ssrRender = ssrCompile(comp.template, instance)
     }
 
-    // perf: enable caching of computed getters during render
-    // since there cannot be state mutations during render.
-    for (const e of instance.scope.effects) {
-      if (e.computed) {
-        e.computed._dirty = true
-        e.computed._cacheable = true
-      }
-    }
-
     const ssrRender = instance.ssrRender || comp.ssrRender
     if (ssrRender) {
       // optimized