]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(reactivity): refactor reactivity core by porting alien-signals (#12349)
authorJohnson Chu <johnsoncodehk@gmail.com>
Mon, 2 Dec 2024 13:05:12 +0000 (21:05 +0800)
committerGitHub <noreply@github.com>
Mon, 2 Dec 2024 13:05:12 +0000 (21:05 +0800)
16 files changed:
packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/__tests__/effect.spec.ts
packages/reactivity/__tests__/gc.spec.ts
packages/reactivity/src/arrayInstrumentations.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/debug.ts [new file with mode: 0644]
packages/reactivity/src/dep.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/effectScope.ts
packages/reactivity/src/ref.ts
packages/reactivity/src/system.ts [new file with mode: 0644]
packages/reactivity/src/watch.ts
packages/runtime-core/__tests__/apiSetupHelpers.spec.ts
packages/runtime-core/__tests__/errorHandling.spec.ts
packages/runtime-core/src/renderer.ts
vitest.config.ts

index 123df44f2539ab13fcb45ea42440d02cdb3b3e38..1e807df17a02c514b8470ea7e95dbc6ce66bf9e6 100644 (file)
@@ -25,8 +25,9 @@ import {
   toRaw,
   triggerRef,
 } from '../src'
-import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
 import type { ComputedRef, ComputedRefImpl } from '../src/computed'
+import { pauseTracking, resetTracking } from '../src/effect'
+import { SubscriberFlags } from '../src/system'
 
 describe('reactivity/computed', () => {
   it('should return updated value', () => {
@@ -409,9 +410,9 @@ describe('reactivity/computed', () => {
     a.value++
     e.value
 
-    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(e.deps!.dep).toBe(b)
+    expect(e.deps!.nextDep!.dep).toBe(d)
+    expect(e.deps!.nextDep!.nextDep!.dep).toBe(c)
     expect(cSpy).toHaveBeenCalledTimes(2)
 
     a.value++
@@ -466,8 +467,8 @@ describe('reactivity/computed', () => {
     const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
 
     c2.value
-    expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
-    expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
+    expect(c1.flags & SubscriberFlags.Dirtys).toBe(0)
+    expect(c2.flags & SubscriberFlags.Dirtys).toBe(0)
   })
 
   it('should chained computeds dirtyLevel update with first computed effect', () => {
index 242fc7071536ed6577670546b19f7f6e0793ad75..20f0244a7bc8fd3d3d4860298fd72119da97bb1d 100644 (file)
@@ -1,3 +1,14 @@
+import {
+  computed,
+  h,
+  nextTick,
+  nodeOps,
+  ref,
+  render,
+  serializeInner,
+} from '@vue/runtime-test'
+import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
+import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
 import {
   type DebuggerEvent,
   type ReactiveEffectRunner,
@@ -11,23 +22,7 @@ import {
   stop,
   toRaw,
 } from '../src/index'
-import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
-import {
-  computed,
-  h,
-  nextTick,
-  nodeOps,
-  ref,
-  render,
-  serializeInner,
-} from '@vue/runtime-test'
-import {
-  endBatch,
-  onEffectCleanup,
-  pauseTracking,
-  resetTracking,
-  startBatch,
-} from '../src/effect'
+import { type Dependency, endBatch, startBatch } from '../src/system'
 
 describe('reactivity/effect', () => {
   it('should run the passed function once (wrapped by a effect)', () => {
@@ -1183,12 +1178,12 @@ describe('reactivity/effect', () => {
   })
 
   describe('dep unsubscribe', () => {
-    function getSubCount(dep: Dep | undefined) {
+    function getSubCount(dep: Dependency | undefined) {
       let count = 0
       let sub = dep!.subs
       while (sub) {
         count++
-        sub = sub.prevSub
+        sub = sub.nextSub
       }
       return count
     }
index a609958409f0ce4f334e4ac16f2413370c4bf0a5..55499ec0a5e4107a67c56ffecf74c6898d393222 100644 (file)
@@ -2,6 +2,7 @@ import {
   type ComputedRef,
   computed,
   effect,
+  effectScope,
   reactive,
   shallowRef as ref,
   toRaw,
@@ -19,7 +20,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
   }
 
   // #9233
-  it('should release computed cache', async () => {
+  it.todo('should release computed cache', async () => {
     const src = ref<{} | undefined>({})
     // @ts-expect-error ES2021 API
     const srcRef = new WeakRef(src.value!)
@@ -34,7 +35,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
     expect(srcRef.deref()).toBeUndefined()
   })
 
-  it('should release reactive property dep', async () => {
+  it.todo('should release reactive property dep', async () => {
     const src = reactive({ foo: 1 })
 
     let c: ComputedRef | undefined = computed(() => src.foo)
@@ -79,4 +80,36 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
     src.foo++
     expect(spy).toHaveBeenCalledTimes(2)
   })
+
+  it('should release computed that untrack by effect', async () => {
+    const src = ref(0)
+    // @ts-expect-error ES2021 API
+    const c = new WeakRef(computed(() => src.value))
+    const scope = effectScope()
+
+    scope.run(() => {
+      effect(() => c.deref().value)
+    })
+
+    expect(c.deref()).toBeDefined()
+    scope.stop()
+    await gc()
+    expect(c.deref()).toBeUndefined()
+  })
+
+  it('should release computed that untrack by effectScope', async () => {
+    const src = ref(0)
+    // @ts-expect-error ES2021 API
+    const c = new WeakRef(computed(() => src.value))
+    const scope = effectScope()
+
+    scope.run(() => {
+      c.deref().value
+    })
+
+    expect(c.deref()).toBeDefined()
+    scope.stop()
+    await gc()
+    expect(c.deref()).toBeUndefined()
+  })
 })
index e031df4fe10774e8e1c828d10484a47cb416ab10..8d578c7d860d62304783a629165392a609d91d4f 100644 (file)
@@ -1,8 +1,9 @@
+import { isArray } from '@vue/shared'
 import { TrackOpTypes } from './constants'
-import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
-import { isProxy, isShallow, toRaw, toReactive } from './reactive'
 import { ARRAY_ITERATE_KEY, track } from './dep'
-import { isArray } from '@vue/shared'
+import { pauseTracking, resetTracking } from './effect'
+import { isProxy, isShallow, toRaw, toReactive } from './reactive'
+import { endBatch, startBatch } from './system'
 
 /**
  * Track array iteration and return:
index ea798e201d4bfc7d4d44df09143586883b7c6f8d..12f2b249aa009009d92313d0ad4fe87fa9ce1eda 100644 (file)
@@ -1,17 +1,27 @@
-import { isFunction } from '@vue/shared'
+import { hasChanged, isFunction } from '@vue/shared'
+import { ReactiveFlags, TrackOpTypes } from './constants'
+import { onTrack, setupFlagsHandler } from './debug'
 import {
   type DebuggerEvent,
   type DebuggerOptions,
-  EffectFlags,
-  type Subscriber,
   activeSub,
-  batch,
-  refreshComputed,
+  activeTrackId,
+  nextTrackId,
+  setActiveSub,
 } from './effect'
+import { activeEffectScope } from './effectScope'
 import type { Ref } from './ref'
+import {
+  type Dependency,
+  type IComputed,
+  type Link,
+  SubscriberFlags,
+  checkDirty,
+  endTrack,
+  link,
+  startTrack,
+} from './system'
 import { warn } from './warning'
-import { Dep, type Link, globalVersion } from './dep'
-import { ReactiveFlags, TrackOpTypes } from './constants'
 
 declare const ComputedRefSymbol: unique symbol
 declare const WritableComputedRefSymbol: unique symbol
@@ -44,15 +54,23 @@ export interface WritableComputedOptions<T, S = T> {
  * @private exported by @vue/reactivity for Vue core use, but not exported from
  * the main vue package
  */
-export class ComputedRefImpl<T = any> implements Subscriber {
+export class ComputedRefImpl<T = any> implements IComputed {
   /**
    * @internal
    */
-  _value: any = undefined
-  /**
-   * @internal
-   */
-  readonly dep: Dep = new Dep(this)
+  _value: T | undefined = undefined
+  version = 0
+
+  // Dependency
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  lastTrackedId = 0
+
+  // Subscriber
+  deps: Link | undefined = undefined
+  depsTail: Link | undefined = undefined
+  flags: SubscriberFlags = SubscriberFlags.Dirty
+
   /**
    * @internal
    */
@@ -63,34 +81,39 @@ export class ComputedRefImpl<T = any> implements Subscriber {
    */
   readonly __v_isReadonly: boolean
   // TODO isolatedDeclarations ReactiveFlags.IS_READONLY
-  // A computed is also a subscriber that tracks other deps
-  /**
-   * @internal
-   */
-  deps?: Link = undefined
-  /**
-   * @internal
-   */
-  depsTail?: Link = undefined
-  /**
-   * @internal
-   */
-  flags: EffectFlags = EffectFlags.DIRTY
-  /**
-   * @internal
-   */
-  globalVersion: number = globalVersion - 1
-  /**
-   * @internal
-   */
-  isSSR: boolean
-  /**
-   * @internal
-   */
-  next?: Subscriber = undefined
 
   // for backwards compat
-  effect: this = this
+  get effect(): this {
+    return this
+  }
+  // for backwards compat
+  get dep(): Dependency {
+    return this
+  }
+  // for backwards compat
+  get _dirty(): boolean {
+    const flags = this.flags
+    if (flags & SubscriberFlags.Dirty) {
+      return true
+    } else if (flags & SubscriberFlags.ToCheckDirty) {
+      if (checkDirty(this.deps!)) {
+        this.flags |= SubscriberFlags.Dirty
+        return true
+      } else {
+        this.flags &= ~SubscriberFlags.ToCheckDirty
+        return false
+      }
+    }
+    return false
+  }
+  set _dirty(v: boolean) {
+    if (v) {
+      this.flags |= SubscriberFlags.Dirty
+    } else {
+      this.flags &= ~SubscriberFlags.Dirtys
+    }
+  }
+
   // dev only
   onTrack?: (event: DebuggerEvent) => void
   // dev only
@@ -105,43 +128,34 @@ export class ComputedRefImpl<T = any> implements Subscriber {
   constructor(
     public fn: ComputedGetter<T>,
     private readonly setter: ComputedSetter<T> | undefined,
-    isSSR: boolean,
   ) {
     this[ReactiveFlags.IS_READONLY] = !setter
-    this.isSSR = isSSR
-  }
-
-  /**
-   * @internal
-   */
-  notify(): true | void {
-    this.flags |= EffectFlags.DIRTY
-    if (
-      !(this.flags & EffectFlags.NOTIFIED) &&
-      // avoid infinite self recursion
-      activeSub !== this
-    ) {
-      batch(this, true)
-      return true
-    } else if (__DEV__) {
-      // TODO warn
+    if (__DEV__) {
+      setupFlagsHandler(this)
     }
   }
 
   get value(): T {
-    const link = __DEV__
-      ? this.dep.track({
+    if (this._dirty) {
+      this.update()
+    }
+    if (activeTrackId !== 0 && this.lastTrackedId !== activeTrackId) {
+      if (__DEV__) {
+        onTrack(activeSub!, {
           target: this,
           type: TrackOpTypes.GET,
           key: 'value',
         })
-      : this.dep.track()
-    refreshComputed(this)
-    // sync version after evaluation
-    if (link) {
-      link.version = this.dep.version
+      }
+      this.lastTrackedId = activeTrackId
+      link(this, activeSub!).version = this.version
+    } else if (
+      activeEffectScope !== undefined &&
+      this.lastTrackedId !== activeEffectScope.trackId
+    ) {
+      link(this, activeEffectScope)
     }
-    return this._value
+    return this._value!
   }
 
   set value(newValue) {
@@ -151,6 +165,27 @@ export class ComputedRefImpl<T = any> implements Subscriber {
       warn('Write operation failed: computed value is readonly')
     }
   }
+
+  update(): boolean {
+    const prevSub = activeSub
+    const prevTrackId = activeTrackId
+    setActiveSub(this, nextTrackId())
+    startTrack(this)
+    const oldValue = this._value
+    let newValue: T
+    try {
+      newValue = this.fn(oldValue)
+    } finally {
+      setActiveSub(prevSub, prevTrackId)
+      endTrack(this)
+    }
+    if (hasChanged(oldValue, newValue)) {
+      this._value = newValue
+      this.version++
+      return true
+    }
+    return false
+  }
 }
 
 /**
@@ -209,7 +244,7 @@ export function computed<T>(
     setter = getterOrOptions.set
   }
 
-  const cRef = new ComputedRefImpl(getter, setter, isSSR)
+  const cRef = new ComputedRefImpl(getter, setter)
 
   if (__DEV__ && debugOptions && !isSSR) {
     cRef.onTrack = debugOptions.onTrack
diff --git a/packages/reactivity/src/debug.ts b/packages/reactivity/src/debug.ts
new file mode 100644 (file)
index 0000000..41908a0
--- /dev/null
@@ -0,0 +1,72 @@
+import { extend } from '@vue/shared'
+import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect'
+import { type Link, type Subscriber, SubscriberFlags } from './system'
+
+export const triggerEventInfos: DebuggerEventExtraInfo[] = []
+
+export function onTrack(
+  sub: Link['sub'],
+  debugInfo: DebuggerEventExtraInfo,
+): void {
+  if (!__DEV__) {
+    throw new Error(
+      `Internal error: onTrack should be called only in development.`,
+    )
+  }
+  if ((sub as ReactiveEffectOptions).onTrack) {
+    ;(sub as ReactiveEffectOptions).onTrack!(
+      extend(
+        {
+          effect: sub,
+        },
+        debugInfo,
+      ),
+    )
+  }
+}
+
+export function onTrigger(sub: Link['sub']): void {
+  if (!__DEV__) {
+    throw new Error(
+      `Internal error: onTrigger should be called only in development.`,
+    )
+  }
+  if ((sub as ReactiveEffectOptions).onTrigger) {
+    const debugInfo = triggerEventInfos[triggerEventInfos.length - 1]
+    ;(sub as ReactiveEffectOptions).onTrigger!(
+      extend(
+        {
+          effect: sub,
+        },
+        debugInfo,
+      ),
+    )
+  }
+}
+
+export function setupFlagsHandler(target: Subscriber): void {
+  if (!__DEV__) {
+    throw new Error(
+      `Internal error: setupFlagsHandler should be called only in development.`,
+    )
+  }
+  // @ts-expect-error
+  target._flags = target.flags
+  Object.defineProperty(target, 'flags', {
+    get() {
+      // @ts-expect-error
+      return target._flags
+    },
+    set(value) {
+      if (
+        // @ts-expect-error
+        !(target._flags >> SubscriberFlags.DirtyFlagsIndex) &&
+        !!(value >> SubscriberFlags.DirtyFlagsIndex)
+      ) {
+        onTrigger(this)
+      }
+      // @ts-expect-error
+      target._flags = value
+    },
+  })
+}
index 196c2aaf98ed7a2c372c1e82c6df0c4974d5d7cb..5c9b84739d4bc7f94049dc6a491548c1c1ecfa8c 100644 (file)
-import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
-import type { ComputedRefImpl } from './computed'
+import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
 import { type TrackOpTypes, TriggerOpTypes } from './constants'
+import { onTrack, triggerEventInfos } from './debug'
+import { activeSub, activeTrackId } from './effect'
 import {
-  type DebuggerEventExtraInfo,
-  EffectFlags,
-  type Subscriber,
-  activeSub,
+  type Dependency,
+  type Link,
   endBatch,
-  shouldTrack,
+  link,
+  propagate,
   startBatch,
-} from './effect'
+} from './system'
 
-/**
- * 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
-
-/**
- * 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 class Link {
-  /**
-   * - 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
+class Dep implements Dependency {
+  _subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  lastTrackedId = 0
 
   constructor(
-    public sub: Subscriber,
-    public dep: Dep,
-  ) {
-    this.version = dep.version
-    this.nextDep =
-      this.prevDep =
-      this.nextSub =
-      this.prevSub =
-      this.prevActiveLink =
-        undefined
-  }
-}
-
-/**
- * @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
-
-  /**
-   * Doubly linked list representing the subscribing effects (head)
-   * DEV only, for invoking onTrigger hooks in correct order
-   */
-  subsHead?: Link
-
-  /**
-   * For object property deps cleanup
-   */
-  map?: KeyToDepMap = undefined
-  key?: unknown = undefined
+    private map: KeyToDepMap,
+    private key: unknown,
+  ) {}
 
-  /**
-   * Subscriber counter
-   */
-  sc: number = 0
-
-  constructor(public computed?: ComputedRefImpl | undefined) {
-    if (__DEV__) {
-      this.subsHead = undefined
-    }
+  get subs(): Link | undefined {
+    return this._subs
   }
 
-  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
-    if (!activeSub || !shouldTrack || activeSub === this.computed) {
-      return
-    }
-
-    let link = this.activeLink
-    if (link === undefined || link.sub !== activeSub) {
-      link = this.activeLink = new Link(activeSub, this)
-
-      // 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
-      }
-
-      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): void {
-    this.version++
-    globalVersion++
-    this.notify(debugInfo)
-  }
-
-  notify(debugInfo?: DebuggerEventExtraInfo): void {
-    startBatch()
-    try {
-      if (__DEV__) {
-        // subs are notified and batched in reverse-order and then invoked in
-        // original order at the end of the batch, but onTrigger hooks should
-        // be invoked in original order here.
-        for (let head = this.subsHead; head; head = head.nextSub) {
-          if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
-            head.sub.onTrigger(
-              extend(
-                {
-                  effect: head.sub,
-                },
-                debugInfo,
-              ),
-            )
-          }
-        }
-      }
-      for (let link = this.subs; link; link = link.prevSub) {
-        if (link.sub.notify()) {
-          // if notify() returns `true`, this is a computed. Also call notify
-          // on its dep - it's called here instead of inside computed's notify
-          // in order to reduce call stack depth.
-          ;(link.sub as ComputedRefImpl).dep.notify()
-        }
-      }
-    } finally {
-      endBatch()
+  set subs(value: Link | undefined) {
+    this._subs = value
+    if (value === undefined) {
+      this.map.delete(this.key)
     }
   }
 }
 
-function addSub(link: Link) {
-  link.dep.sc++
-  if (link.sub.flags & EffectFlags.TRACKING) {
-    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
-    }
-
-    if (__DEV__ && link.dep.subsHead === undefined) {
-      link.dep.subsHead = 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
@@ -254,25 +62,25 @@ export const ARRAY_ITERATE_KEY: unique symbol = Symbol(
  * @param key - Identifier of the reactive property to track.
  */
 export function track(target: object, type: TrackOpTypes, key: unknown): void {
-  if (shouldTrack && activeSub) {
+  if (activeTrackId > 0) {
     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()))
-      dep.map = depsMap
-      dep.key = key
+      depsMap.set(key, (dep = new Dep(depsMap, key)))
     }
-    if (__DEV__) {
-      dep.track({
-        target,
-        type,
-        key,
-      })
-    } else {
-      dep.track()
+    if (dep.lastTrackedId !== activeTrackId) {
+      if (__DEV__) {
+        onTrack(activeSub!, {
+          target,
+          type,
+          key,
+        })
+      }
+      dep.lastTrackedId = activeTrackId
+      link(dep, activeSub!)
     }
   }
 }
@@ -296,14 +104,13 @@ export function trigger(
   const depsMap = targetMap.get(target)
   if (!depsMap) {
     // never been tracked
-    globalVersion++
     return
   }
 
-  const run = (dep: Dep | undefined) => {
-    if (dep) {
+  const run = (dep: Dependency | undefined) => {
+    if (dep !== undefined && dep.subs !== undefined) {
       if (__DEV__) {
-        dep.trigger({
+        triggerEventInfos.push({
           target,
           type,
           key,
@@ -311,8 +118,10 @@ export function trigger(
           oldValue,
           oldTarget,
         })
-      } else {
-        dep.trigger()
+      }
+      propagate(dep.subs)
+      if (__DEV__) {
+        triggerEventInfos.pop()
       }
     }
   }
@@ -385,7 +194,7 @@ export function trigger(
 export function getDepFromReactive(
   object: any,
   key: string | number | symbol,
-): Dep | undefined {
+): Dependency | undefined {
   const depMap = targetMap.get(object)
   return depMap && depMap.get(key)
 }
index 886f380dd521db5af93da8d4bfac31ec0a919db5..d0aa92b330ad71a89ca9307b5f23c3d0e1792fc5 100644 (file)
@@ -1,8 +1,16 @@
-import { extend, hasChanged } from '@vue/shared'
-import type { ComputedRefImpl } from './computed'
+import { extend } from '@vue/shared'
 import type { TrackOpTypes, TriggerOpTypes } from './constants'
-import { type Link, globalVersion } from './dep'
+import { setupFlagsHandler } from './debug'
 import { activeEffectScope } from './effectScope'
+import {
+  type IEffect,
+  type Link,
+  type Subscriber,
+  SubscriberFlags,
+  checkDirty,
+  endTrack,
+  startTrack,
+} from './system'
 import { warn } from './warning'
 
 export type EffectScheduler = (...args: any[]) => any
@@ -27,7 +35,6 @@ export interface DebuggerOptions {
 
 export interface ReactiveEffectOptions extends DebuggerOptions {
   scheduler?: EffectScheduler
-  allowRecurse?: boolean
   onStop?: () => void
 }
 
@@ -36,78 +43,29 @@ export interface ReactiveEffectRunner<T = any> {
   effect: ReactiveEffect
 }
 
-export let activeSub: Subscriber | undefined
-
 export enum EffectFlags {
   /**
    * ReactiveEffect only
    */
-  ACTIVE = 1 << 0,
-  RUNNING = 1 << 1,
-  TRACKING = 1 << 2,
-  NOTIFIED = 1 << 3,
-  DIRTY = 1 << 4,
-  ALLOW_RECURSE = 1 << 5,
-  PAUSED = 1 << 6,
+  ALLOW_RECURSE = 1 << 2,
+  PAUSED = 1 << 3,
+  NOTIFIED = 1 << 4,
+  STOP = 1 << 5,
 }
 
-/**
- * Subscriber is a type that tracks (or subscribes to) a list of deps.
- */
-export interface Subscriber extends DebuggerOptions {
-  /**
-   * Head of the doubly linked list representing the deps
-   * @internal
-   */
-  deps?: Link
-  /**
-   * Tail of the same list
-   * @internal
-   */
-  depsTail?: Link
-  /**
-   * @internal
-   */
-  flags: EffectFlags
-  /**
-   * @internal
-   */
-  next?: Subscriber
-  /**
-   * returning `true` indicates it's a computed that needs to call notify
-   * on its dep too
-   * @internal
-   */
-  notify(): true | void
-}
+export class ReactiveEffect<T = any> implements IEffect, ReactiveEffectOptions {
+  nextNotify: IEffect | undefined = undefined
 
-const pausedQueueEffects = new WeakSet<ReactiveEffect>()
+  // Subscriber
+  deps: Link | undefined = undefined
+  depsTail: Link | undefined = undefined
+  flags: number = SubscriberFlags.Dirty
 
-export class ReactiveEffect<T = any>
-  implements Subscriber, ReactiveEffectOptions
-{
-  /**
-   * @internal
-   */
-  deps?: Link = undefined
-  /**
-   * @internal
-   */
-  depsTail?: Link = undefined
-  /**
-   * @internal
-   */
-  flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
-  /**
-   * @internal
-   */
-  next?: Subscriber = undefined
   /**
    * @internal
    */
   cleanup?: () => void = undefined
 
-  scheduler?: EffectScheduler = undefined
   onStop?: () => void
   onTrack?: (event: DebuggerEvent) => void
   onTrigger?: (event: DebuggerEvent) => void
@@ -116,52 +74,59 @@ export class ReactiveEffect<T = any>
     if (activeEffectScope && activeEffectScope.active) {
       activeEffectScope.effects.push(this)
     }
+    if (__DEV__) {
+      setupFlagsHandler(this)
+    }
+  }
+
+  get active(): boolean {
+    return !(this.flags & EffectFlags.STOP)
   }
 
   pause(): void {
-    this.flags |= EffectFlags.PAUSED
+    if (!(this.flags & EffectFlags.PAUSED)) {
+      this.flags |= EffectFlags.PAUSED
+    }
   }
 
   resume(): void {
-    if (this.flags & EffectFlags.PAUSED) {
+    const flags = this.flags
+    if (flags & EffectFlags.PAUSED) {
       this.flags &= ~EffectFlags.PAUSED
-      if (pausedQueueEffects.has(this)) {
-        pausedQueueEffects.delete(this)
-        this.trigger()
-      }
+    }
+    if (flags & EffectFlags.NOTIFIED) {
+      this.flags &= ~EffectFlags.NOTIFIED
+      this.notify()
     }
   }
 
-  /**
-   * @internal
-   */
   notify(): void {
-    if (
-      this.flags & EffectFlags.RUNNING &&
-      !(this.flags & EffectFlags.ALLOW_RECURSE)
-    ) {
-      return
+    const flags = this.flags
+    if (!(flags & EffectFlags.PAUSED)) {
+      this.scheduler()
+    } else {
+      this.flags |= EffectFlags.NOTIFIED
     }
-    if (!(this.flags & EffectFlags.NOTIFIED)) {
-      batch(this)
+  }
+
+  scheduler(): void {
+    if (this.dirty) {
+      this.run()
     }
   }
 
   run(): T {
     // TODO cleanupEffect
 
-    if (!(this.flags & EffectFlags.ACTIVE)) {
+    if (!this.active) {
       // stopped during cleanup
       return this.fn()
     }
-
-    this.flags |= EffectFlags.RUNNING
     cleanupEffect(this)
-    prepareDeps(this)
-    const prevEffect = activeSub
-    const prevShouldTrack = shouldTrack
-    activeSub = this
-    shouldTrack = true
+    const prevSub = activeSub
+    const prevTrackId = activeTrackId
+    setActiveSub(this, nextTrackId())
+    startTrack(this)
 
     try {
       return this.fn()
@@ -172,299 +137,42 @@ export class ReactiveEffect<T = any>
             'this is likely a Vue internal bug.',
         )
       }
-      cleanupDeps(this)
-      activeSub = prevEffect
-      shouldTrack = prevShouldTrack
-      this.flags &= ~EffectFlags.RUNNING
+      setActiveSub(prevSub, prevTrackId)
+      endTrack(this)
+      if (
+        this.flags & SubscriberFlags.CanPropagate &&
+        this.flags & EffectFlags.ALLOW_RECURSE
+      ) {
+        this.flags &= ~SubscriberFlags.CanPropagate
+        this.notify()
+      }
     }
   }
 
   stop(): void {
-    if (this.flags & EffectFlags.ACTIVE) {
-      for (let link = this.deps; link; link = link.nextDep) {
-        removeSub(link)
-      }
-      this.deps = this.depsTail = undefined
+    if (this.active) {
+      startTrack(this)
+      endTrack(this)
       cleanupEffect(this)
       this.onStop && this.onStop()
-      this.flags &= ~EffectFlags.ACTIVE
-    }
-  }
-
-  trigger(): void {
-    if (this.flags & EffectFlags.PAUSED) {
-      pausedQueueEffects.add(this)
-    } else if (this.scheduler) {
-      this.scheduler()
-    } else {
-      this.runIfDirty()
-    }
-  }
-
-  /**
-   * @internal
-   */
-  runIfDirty(): void {
-    if (isDirty(this)) {
-      this.run()
+      this.flags |= EffectFlags.STOP
     }
   }
 
   get dirty(): boolean {
-    return isDirty(this)
-  }
-}
-
-/**
- * For debugging
- */
-// function printDeps(sub: Subscriber) {
-//   let d = sub.deps
-//   let ds = []
-//   while (d) {
-//     ds.push(d)
-//     d = d.nextDep
-//   }
-//   return ds.map(d => ({
-//     id: d.id,
-//     prev: d.prevDep?.id,
-//     next: d.nextDep?.id,
-//   }))
-// }
-
-let batchDepth = 0
-let batchedSub: Subscriber | undefined
-let batchedComputed: Subscriber | undefined
-
-export function batch(sub: Subscriber, isComputed = false): void {
-  sub.flags |= EffectFlags.NOTIFIED
-  if (isComputed) {
-    sub.next = batchedComputed
-    batchedComputed = sub
-    return
-  }
-  sub.next = batchedSub
-  batchedSub = sub
-}
-
-/**
- * @internal
- */
-export function startBatch(): void {
-  batchDepth++
-}
-
-/**
- * Run batched effects when all batches have ended
- * @internal
- */
-export function endBatch(): void {
-  if (--batchDepth > 0) {
-    return
-  }
-
-  if (batchedComputed) {
-    let e: Subscriber | undefined = batchedComputed
-    batchedComputed = undefined
-    while (e) {
-      const next: Subscriber | undefined = e.next
-      e.next = undefined
-      e.flags &= ~EffectFlags.NOTIFIED
-      e = next
-    }
-  }
-
-  let error: unknown
-  while (batchedSub) {
-    let e: Subscriber | undefined = batchedSub
-    batchedSub = undefined
-    while (e) {
-      const next: Subscriber | undefined = e.next
-      e.next = undefined
-      e.flags &= ~EffectFlags.NOTIFIED
-      if (e.flags & EffectFlags.ACTIVE) {
-        try {
-          // ACTIVE flag is effect-only
-          ;(e as ReactiveEffect).trigger()
-        } catch (err) {
-          if (!error) error = err
-        }
-      }
-      e = next
-    }
-  }
-
-  if (error) throw error
-}
-
-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 cleanupDeps(sub: Subscriber) {
-  // Cleanup unsued deps
-  let head
-  let tail = sub.depsTail
-  let link = tail
-  while (link) {
-    const prev = link.prevDep
-    if (link.version === -1) {
-      if (link === tail) tail = prev
-      // 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
-    link = prev
-  }
-  // set the new head & tail
-  sub.deps = head
-  sub.depsTail = tail
-}
-
-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) ||
-          link.dep.version !== link.version))
-    ) {
+    const flags = this.flags
+    if (flags & SubscriberFlags.Dirty) {
       return true
-    }
-  }
-  // @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
-}
-
-/**
- * Returning false indicates the refresh failed
- * @internal
- */
-export function refreshComputed(computed: ComputedRefImpl): undefined {
-  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 &&
-    computed.deps &&
-    !isDirty(computed)
-  ) {
-    computed.flags &= ~EffectFlags.RUNNING
-    return
-  }
-
-  const prevSub = activeSub
-  const prevShouldTrack = shouldTrack
-  activeSub = computed
-  shouldTrack = true
-
-  try {
-    prepareDeps(computed)
-    const value = computed.fn(computed._value)
-    if (dep.version === 0 || hasChanged(value, computed._value)) {
-      computed._value = value
-      dep.version++
-    }
-  } catch (err) {
-    dep.version++
-    throw err
-  } finally {
-    activeSub = prevSub
-    shouldTrack = prevShouldTrack
-    cleanupDeps(computed)
-    computed.flags &= ~EffectFlags.RUNNING
-  }
-}
-
-function removeSub(link: Link, soft = false) {
-  const { dep, prevSub, nextSub } = link
-  if (prevSub) {
-    prevSub.nextSub = nextSub
-    link.prevSub = undefined
-  }
-  if (nextSub) {
-    nextSub.prevSub = prevSub
-    link.nextSub = undefined
-  }
-  if (__DEV__ && dep.subsHead === link) {
-    // was previous head, point new head to next
-    dep.subsHead = nextSub
-  }
-
-  if (dep.subs === link) {
-    // was previous tail, point new tail to prev
-    dep.subs = prevSub
-
-    if (!prevSub && dep.computed) {
-      // 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) {
-        // here we are only "soft" unsubscribing because the computed still keeps
-        // referencing the deps and the dep should not decrease its sub count
-        removeSub(l, true)
+    } else if (flags & SubscriberFlags.ToCheckDirty) {
+      if (checkDirty(this.deps!)) {
+        this.flags |= SubscriberFlags.Dirty
+        return true
+      } else {
+        this.flags &= ~SubscriberFlags.ToCheckDirty
+        return false
       }
     }
-  }
-
-  if (!soft && !--dep.sc && dep.map) {
-    // #11979
-    // property dep no longer has effect subscribers, delete it
-    // this mostly is for the case where an object is kept in memory but only a
-    // subset of its properties is tracked at one time
-    dep.map.delete(dep.key)
-  }
-}
-
-function removeDep(link: Link) {
-  const { prevDep, nextDep } = link
-  if (prevDep) {
-    prevDep.nextDep = nextDep
-    link.prevDep = undefined
-  }
-  if (nextDep) {
-    nextDep.prevDep = prevDep
-    link.nextDep = undefined
+    return false
   }
 }
 
@@ -505,34 +213,55 @@ export function stop(runner: ReactiveEffectRunner): void {
   runner.effect.stop()
 }
 
-/**
- * @internal
- */
-export let shouldTrack = true
-const trackStack: boolean[] = []
+const resetTrackingStack: [sub: typeof activeSub, trackId: number][] = []
 
 /**
  * Temporarily pauses tracking.
  */
 export function pauseTracking(): void {
-  trackStack.push(shouldTrack)
-  shouldTrack = false
+  resetTrackingStack.push([activeSub, activeTrackId])
+  activeSub = undefined
+  activeTrackId = 0
 }
 
 /**
  * Re-enables effect tracking (if it was paused).
  */
 export function enableTracking(): void {
-  trackStack.push(shouldTrack)
-  shouldTrack = true
+  const isPaused = activeSub === undefined
+  if (!isPaused) {
+    // Add the current active effect to the trackResetStack so it can be
+    // restored by calling resetTracking.
+    resetTrackingStack.push([activeSub, activeTrackId])
+  } else {
+    // Add a placeholder to the trackResetStack so we can it can be popped
+    // to restore the previous active effect.
+    resetTrackingStack.push([undefined, 0])
+    for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
+      if (resetTrackingStack[i][0] !== undefined) {
+        ;[activeSub, activeTrackId] = resetTrackingStack[i]
+        break
+      }
+    }
+  }
 }
 
 /**
  * Resets the previous global effect tracking state.
  */
 export function resetTracking(): void {
-  const last = trackStack.pop()
-  shouldTrack = last === undefined ? true : last
+  if (__DEV__ && resetTrackingStack.length === 0) {
+    warn(
+      `resetTracking() was called when there was no active tracking ` +
+        `to reset.`,
+    )
+  }
+  if (resetTrackingStack.length) {
+    ;[activeSub, activeTrackId] = resetTrackingStack.pop()!
+  } else {
+    activeSub = undefined
+    activeTrackId = 0
+  }
 }
 
 /**
@@ -561,7 +290,7 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
 function cleanupEffect(e: ReactiveEffect) {
   const { cleanup } = e
   e.cleanup = undefined
-  if (cleanup) {
+  if (cleanup !== undefined) {
     // run cleanup without active effect
     const prevSub = activeSub
     activeSub = undefined
@@ -572,3 +301,16 @@ function cleanupEffect(e: ReactiveEffect) {
     }
   }
 }
+
+export let activeSub: Subscriber | undefined = undefined
+export let activeTrackId = 0
+export let lastTrackId = 0
+export const nextTrackId = (): number => ++lastTrackId
+
+export function setActiveSub(
+  sub: Subscriber | undefined,
+  trackId: number,
+): void {
+  activeSub = sub
+  activeTrackId = trackId
+}
index cb4e057c480fcbee3b695142a1cdc46854c0af56..b03cbc2800f750fdc959a8968606a8398c919fbc 100644 (file)
@@ -1,13 +1,23 @@
-import type { ReactiveEffect } from './effect'
+import { EffectFlags, type ReactiveEffect, nextTrackId } from './effect'
+import {
+  type Link,
+  type Subscriber,
+  SubscriberFlags,
+  endTrack,
+  startTrack,
+} from './system'
 import { warn } from './warning'
 
 export let activeEffectScope: EffectScope | undefined
 
-export class EffectScope {
-  /**
-   * @internal
-   */
-  private _active = true
+export class EffectScope implements Subscriber {
+  // Subscriber: In order to collect orphans computeds
+  deps: Link | undefined = undefined
+  depsTail: Link | undefined = undefined
+  flags: number = SubscriberFlags.None
+
+  trackId: number = nextTrackId()
+
   /**
    * @internal
    */
@@ -17,8 +27,6 @@ export class EffectScope {
    */
   cleanups: (() => void)[] = []
 
-  private _isPaused = false
-
   /**
    * only assigned by undetached scope
    * @internal
@@ -47,12 +55,12 @@ export class EffectScope {
   }
 
   get active(): boolean {
-    return this._active
+    return !(this.flags & EffectFlags.STOP)
   }
 
   pause(): void {
-    if (this._active) {
-      this._isPaused = true
+    if (!(this.flags & EffectFlags.PAUSED)) {
+      this.flags |= EffectFlags.PAUSED
       let i, l
       if (this.scopes) {
         for (i = 0, l = this.scopes.length; i < l; i++) {
@@ -69,24 +77,22 @@ export class EffectScope {
    * Resumes the effect scope, including all child scopes and effects.
    */
   resume(): void {
-    if (this._active) {
-      if (this._isPaused) {
-        this._isPaused = false
-        let i, l
-        if (this.scopes) {
-          for (i = 0, l = this.scopes.length; i < l; i++) {
-            this.scopes[i].resume()
-          }
-        }
-        for (i = 0, l = this.effects.length; i < l; i++) {
-          this.effects[i].resume()
+    if (this.flags & EffectFlags.PAUSED) {
+      this.flags &= ~EffectFlags.PAUSED
+      let i, l
+      if (this.scopes) {
+        for (i = 0, l = this.scopes.length; i < l; i++) {
+          this.scopes[i].resume()
         }
       }
+      for (i = 0, l = this.effects.length; i < l; i++) {
+        this.effects[i].resume()
+      }
     }
   }
 
   run<T>(fn: () => T): T | undefined {
-    if (this._active) {
+    if (this.active) {
       const currentEffectScope = activeEffectScope
       try {
         activeEffectScope = this
@@ -116,8 +122,10 @@ export class EffectScope {
   }
 
   stop(fromParent?: boolean): void {
-    if (this._active) {
-      this._active = false
+    if (this.active) {
+      this.flags |= EffectFlags.STOP
+      startTrack(this)
+      endTrack(this)
       let i, l
       for (i = 0, l = this.effects.length; i < l; i++) {
         this.effects[i].stop()
index 6b8d541819d3701f0e4d0ca502346d1e40ae3248..1778ea7ea1e891988adb78fdeff13a7db420de03 100644 (file)
@@ -5,7 +5,11 @@ import {
   isFunction,
   isObject,
 } from '@vue/shared'
-import { Dep, getDepFromReactive } from './dep'
+import type { ComputedRef, WritableComputedRef } from './computed'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
+import { onTrack, triggerEventInfos } from './debug'
+import { getDepFromReactive } from './dep'
+import { activeSub, activeTrackId } from './effect'
 import {
   type Builtin,
   type ShallowReactiveMarker,
@@ -16,8 +20,7 @@ import {
   toRaw,
   toReactive,
 } from './reactive'
-import type { ComputedRef, WritableComputedRef } from './computed'
-import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
+import { type Dependency, type Link, link, propagate } from './system'
 import { warn } from './warning'
 
 declare const RefSymbol: unique symbol
@@ -105,12 +108,15 @@ function createRef(rawValue: unknown, shallow: boolean) {
 /**
  * @internal
  */
-class RefImpl<T = any> {
+class RefImpl<T = any> implements Dependency {
+  // Dependency
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  lastTrackedId = 0
+
   _value: T
   private _rawValue: T
 
-  dep: Dep = new Dep()
-
   public readonly [ReactiveFlags.IS_REF] = true
   public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
 
@@ -120,16 +126,12 @@ class RefImpl<T = any> {
     this[ReactiveFlags.IS_SHALLOW] = isShallow
   }
 
+  get dep() {
+    return this
+  }
+
   get value() {
-    if (__DEV__) {
-      this.dep.track({
-        target: this,
-        type: TrackOpTypes.GET,
-        key: 'value',
-      })
-    } else {
-      this.dep.track()
-    }
+    trackRef(this)
     return this._value
   }
 
@@ -144,15 +146,17 @@ class RefImpl<T = any> {
       this._rawValue = newValue
       this._value = useDirectValue ? newValue : toReactive(newValue)
       if (__DEV__) {
-        this.dep.trigger({
+        triggerEventInfos.push({
           target: this,
           type: TriggerOpTypes.SET,
           key: 'value',
           newValue,
           oldValue,
         })
-      } else {
-        this.dep.trigger()
+      }
+      triggerRef(this as unknown as Ref)
+      if (__DEV__) {
+        triggerEventInfos.pop()
       }
     }
   }
@@ -185,17 +189,23 @@ class RefImpl<T = any> {
  */
 export function triggerRef(ref: Ref): void {
   // ref may be an instance of ObjectRefImpl
-  if ((ref as unknown as RefImpl).dep) {
+  const dep = (ref as unknown as RefImpl).dep
+  if (dep !== undefined && dep.subs !== undefined) {
+    propagate(dep.subs)
+  }
+}
+
+function trackRef(dep: Dependency) {
+  if (activeTrackId !== 0 && dep.lastTrackedId !== activeTrackId) {
     if (__DEV__) {
-      ;(ref as unknown as RefImpl).dep.trigger({
-        target: ref,
-        type: TriggerOpTypes.SET,
+      onTrack(activeSub!, {
+        target: dep,
+        type: TrackOpTypes.GET,
         key: 'value',
-        newValue: (ref as unknown as RefImpl)._value,
       })
-    } else {
-      ;(ref as unknown as RefImpl).dep.trigger()
     }
+    dep.lastTrackedId = activeTrackId
+    link(dep, activeSub!)
   }
 }
 
@@ -287,8 +297,11 @@ export type CustomRefFactory<T> = (
   set: (value: T) => void
 }
 
-class CustomRefImpl<T> {
-  public dep: Dep
+class CustomRefImpl<T> implements Dependency {
+  // Dependency
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  lastTrackedId = 0
 
   private readonly _get: ReturnType<CustomRefFactory<T>>['get']
   private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@@ -298,12 +311,18 @@ class CustomRefImpl<T> {
   public _value: T = undefined!
 
   constructor(factory: CustomRefFactory<T>) {
-    const dep = (this.dep = new Dep())
-    const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
+    const { get, set } = factory(
+      () => trackRef(this),
+      () => triggerRef(this as unknown as Ref),
+    )
     this._get = get
     this._set = set
   }
 
+  get dep() {
+    return this
+  }
+
   get value() {
     return (this._value = this._get())
   }
@@ -366,7 +385,7 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
     this._object[this._key] = newVal
   }
 
-  get dep(): Dep | undefined {
+  get dep(): Dependency | undefined {
     return getDepFromReactive(toRaw(this._object), this._key)
   }
 }
diff --git a/packages/reactivity/src/system.ts b/packages/reactivity/src/system.ts
new file mode 100644 (file)
index 0000000..f34562b
--- /dev/null
@@ -0,0 +1,366 @@
+// Ported from https://github.com/stackblitz/alien-signals/blob/v0.4.4/src/system.ts
+
+export interface IEffect extends Subscriber {
+  nextNotify: IEffect | undefined
+  notify(): void
+}
+
+export interface IComputed extends Dependency, Subscriber {
+  version: number
+  update(): boolean
+}
+
+export interface Dependency {
+  subs: Link | undefined
+  subsTail: Link | undefined
+  lastTrackedId?: number
+}
+
+export interface Subscriber {
+  flags: SubscriberFlags
+  deps: Link | undefined
+  depsTail: Link | undefined
+}
+
+export interface Link {
+  dep: Dependency | IComputed | (Dependency & IEffect)
+  sub: Subscriber | IComputed | (Dependency & IEffect) | IEffect
+  version: number
+  // Reuse to link prev stack in checkDirty
+  // Reuse to link prev stack in propagate
+  prevSub: Link | undefined
+  nextSub: Link | undefined
+  // Reuse to link next released link in linkPool
+  nextDep: Link | undefined
+}
+
+export enum SubscriberFlags {
+  None = 0,
+  Tracking = 1 << 0,
+  CanPropagate = 1 << 1,
+  // RunInnerEffects = 1 << 2, // Not used in Vue
+  // 2~5 are using in EffectFlags
+  ToCheckDirty = 1 << 6,
+  Dirty = 1 << 7,
+  Dirtys = SubscriberFlags.ToCheckDirty | SubscriberFlags.Dirty,
+
+  DirtyFlagsIndex = 6,
+}
+
+let batchDepth = 0
+let queuedEffects: IEffect | undefined
+let queuedEffectsTail: IEffect | undefined
+let linkPool: Link | undefined
+
+export function startBatch(): void {
+  ++batchDepth
+}
+
+export function endBatch(): void {
+  if (!--batchDepth) {
+    drainQueuedEffects()
+  }
+}
+
+function drainQueuedEffects(): void {
+  while (queuedEffects !== undefined) {
+    const effect = queuedEffects
+    const queuedNext = effect.nextNotify
+    if (queuedNext !== undefined) {
+      effect.nextNotify = undefined
+      queuedEffects = queuedNext
+    } else {
+      queuedEffects = undefined
+      queuedEffectsTail = undefined
+    }
+    effect.notify()
+  }
+}
+
+export function link(dep: Dependency, sub: Subscriber): Link {
+  const currentDep = sub.depsTail
+  const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps
+  if (nextDep !== undefined && nextDep.dep === dep) {
+    sub.depsTail = nextDep
+    return nextDep
+  } else {
+    return linkNewDep(dep, sub, nextDep, currentDep)
+  }
+}
+
+function linkNewDep(
+  dep: Dependency,
+  sub: Subscriber,
+  nextDep: Link | undefined,
+  depsTail: Link | undefined,
+): Link {
+  let newLink: Link
+
+  if (linkPool !== undefined) {
+    newLink = linkPool
+    linkPool = newLink.nextDep
+    newLink.nextDep = nextDep
+    newLink.dep = dep
+    newLink.sub = sub
+  } else {
+    newLink = {
+      dep,
+      sub,
+      version: 0,
+      nextDep,
+      prevSub: undefined,
+      nextSub: undefined,
+    }
+  }
+
+  if (depsTail === undefined) {
+    sub.deps = newLink
+  } else {
+    depsTail.nextDep = newLink
+  }
+
+  if (dep.subs === undefined) {
+    dep.subs = newLink
+  } else {
+    const oldTail = dep.subsTail!
+    newLink.prevSub = oldTail
+    oldTail.nextSub = newLink
+  }
+
+  sub.depsTail = newLink
+  dep.subsTail = newLink
+
+  return newLink
+}
+
+export function propagate(subs: Link): void {
+  let targetFlag = SubscriberFlags.Dirty
+  let link = subs
+  let stack = 0
+  let nextSub: Link | undefined
+
+  top: do {
+    const sub = link.sub
+    const subFlags = sub.flags
+
+    if (!(subFlags & SubscriberFlags.Tracking)) {
+      let canPropagate = !(subFlags >> SubscriberFlags.DirtyFlagsIndex)
+      if (!canPropagate && subFlags & SubscriberFlags.CanPropagate) {
+        sub.flags &= ~SubscriberFlags.CanPropagate
+        canPropagate = true
+      }
+      if (canPropagate) {
+        sub.flags |= targetFlag
+        const subSubs = (sub as Dependency).subs
+        if (subSubs !== undefined) {
+          if (subSubs.nextSub !== undefined) {
+            subSubs.prevSub = subs
+            subs = subSubs
+            ++stack
+          }
+          link = subSubs
+          targetFlag = SubscriberFlags.ToCheckDirty
+          continue
+        }
+        if ('notify' in sub) {
+          if (queuedEffectsTail !== undefined) {
+            queuedEffectsTail.nextNotify = sub
+          } else {
+            queuedEffects = sub
+          }
+          queuedEffectsTail = sub
+        }
+      } else if (!(sub.flags & targetFlag)) {
+        sub.flags |= targetFlag
+      }
+    } else if (isValidLink(link, sub)) {
+      if (!(subFlags >> SubscriberFlags.DirtyFlagsIndex)) {
+        sub.flags |= targetFlag | SubscriberFlags.CanPropagate
+        const subSubs = (sub as Dependency).subs
+        if (subSubs !== undefined) {
+          if (subSubs.nextSub !== undefined) {
+            subSubs.prevSub = subs
+            subs = subSubs
+            ++stack
+          }
+          link = subSubs
+          targetFlag = SubscriberFlags.ToCheckDirty
+          continue
+        }
+      } else if (!(sub.flags & targetFlag)) {
+        sub.flags |= targetFlag
+      }
+    }
+
+    if ((nextSub = subs.nextSub) === undefined) {
+      if (stack) {
+        let dep = subs.dep
+        do {
+          --stack
+          const depSubs = dep.subs!
+          const prevLink = depSubs.prevSub!
+          depSubs.prevSub = undefined
+          link = subs = prevLink.nextSub!
+          if (subs !== undefined) {
+            targetFlag = stack
+              ? SubscriberFlags.ToCheckDirty
+              : SubscriberFlags.Dirty
+            continue top
+          }
+          dep = prevLink.dep
+        } while (stack)
+      }
+      break
+    }
+    if (link !== subs) {
+      targetFlag = stack ? SubscriberFlags.ToCheckDirty : SubscriberFlags.Dirty
+    }
+    link = subs = nextSub
+  } while (true)
+
+  if (!batchDepth) {
+    drainQueuedEffects()
+  }
+}
+
+function isValidLink(subLink: Link, sub: Subscriber) {
+  const depsTail = sub.depsTail
+  if (depsTail !== undefined) {
+    let link = sub.deps!
+    do {
+      if (link === subLink) {
+        return true
+      }
+      if (link === depsTail) {
+        break
+      }
+      link = link.nextDep!
+    } while (link !== undefined)
+  }
+  return false
+}
+
+export function checkDirty(deps: Link): boolean {
+  let stack = 0
+  let dirty: boolean
+  let nextDep: Link | undefined
+
+  top: do {
+    dirty = false
+    const dep = deps.dep
+    if ('update' in dep) {
+      if (dep.version !== deps.version) {
+        dirty = true
+      } else {
+        const depFlags = dep.flags
+        if (depFlags & SubscriberFlags.Dirty) {
+          dirty = dep.update()
+        } else if (depFlags & SubscriberFlags.ToCheckDirty) {
+          dep.subs!.prevSub = deps
+          deps = dep.deps!
+          ++stack
+          continue
+        }
+      }
+    }
+    if (dirty || (nextDep = deps.nextDep) === undefined) {
+      if (stack) {
+        let sub = deps.sub as IComputed
+        do {
+          --stack
+          const subSubs = sub.subs!
+          const prevLink = subSubs.prevSub!
+          subSubs.prevSub = undefined
+          if (dirty) {
+            if (sub.update()) {
+              sub = prevLink.sub as IComputed
+              dirty = true
+              continue
+            }
+          } else {
+            sub.flags &= ~SubscriberFlags.Dirtys
+          }
+          deps = prevLink.nextDep!
+          if (deps !== undefined) {
+            continue top
+          }
+          sub = prevLink.sub as IComputed
+          dirty = false
+        } while (stack)
+      }
+      return dirty
+    }
+    deps = nextDep
+  } while (true)
+}
+
+export function startTrack(sub: Subscriber): void {
+  sub.depsTail = undefined
+  sub.flags =
+    (sub.flags & ~(SubscriberFlags.CanPropagate | SubscriberFlags.Dirtys)) |
+    SubscriberFlags.Tracking
+}
+
+export function endTrack(sub: Subscriber): void {
+  const depsTail = sub.depsTail
+  if (depsTail !== undefined) {
+    if (depsTail.nextDep !== undefined) {
+      clearTrack(depsTail.nextDep)
+      depsTail.nextDep = undefined
+    }
+  } else if (sub.deps !== undefined) {
+    clearTrack(sub.deps)
+    sub.deps = undefined
+  }
+  sub.flags &= ~SubscriberFlags.Tracking
+}
+
+function clearTrack(link: Link): void {
+  do {
+    const dep = link.dep
+    const nextDep = link.nextDep
+    const nextSub = link.nextSub
+    const prevSub = link.prevSub
+
+    if (nextSub !== undefined) {
+      nextSub.prevSub = prevSub
+      link.nextSub = undefined
+    } else {
+      dep.subsTail = prevSub
+      if ('lastTrackedId' in dep) {
+        dep.lastTrackedId = 0
+      }
+    }
+
+    if (prevSub !== undefined) {
+      prevSub.nextSub = nextSub
+      link.prevSub = undefined
+    } else {
+      dep.subs = nextSub
+    }
+
+    // @ts-expect-error
+    link.dep = undefined
+    // @ts-expect-error
+    link.sub = undefined
+    link.nextDep = linkPool
+    linkPool = link
+
+    if (dep.subs === undefined && 'deps' in dep) {
+      if ('notify' in dep) {
+        dep.flags &= ~SubscriberFlags.Dirtys
+      } else {
+        dep.flags |= SubscriberFlags.Dirty
+      }
+      const depDeps = dep.deps
+      if (depDeps !== undefined) {
+        link = depDeps
+        dep.depsTail!.nextDep = nextDep
+        dep.deps = undefined
+        dep.depsTail = undefined
+        continue
+      }
+    }
+    link = nextDep!
+  } while (link !== undefined)
+}
index 659121ca34b9544a1b6879cc2fc6ded2cad8adf3..094bf226ca8dfacda865bbea8de47dbe0ccb2d5a 100644 (file)
@@ -10,20 +10,19 @@ import {
   isSet,
   remove,
 } from '@vue/shared'
-import { warn } from './warning'
 import type { ComputedRef } from './computed'
 import { ReactiveFlags } from './constants'
 import {
   type DebuggerOptions,
-  EffectFlags,
   type EffectScheduler,
   ReactiveEffect,
   pauseTracking,
   resetTracking,
 } from './effect'
+import { getCurrentScope } from './effectScope'
 import { isReactive, isShallow } from './reactive'
 import { type Ref, isRef } from './ref'
-import { getCurrentScope } from './effectScope'
+import { warn } from './warning'
 
 // These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
 // to @vue/reactivity to allow co-location with the moved base watch logic, hence
@@ -231,10 +230,7 @@ export function watch(
     : INITIAL_WATCHER_VALUE
 
   const job = (immediateFirstRun?: boolean) => {
-    if (
-      !(effect.flags & EffectFlags.ACTIVE) ||
-      (!effect.dirty && !immediateFirstRun)
-    ) {
+    if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
       return
     }
     if (cb) {
index 30c8951f40562f20292700b37225119d401f79d9..5cc5a21caf04b0195d4fd99c6e87e4d044520c4f 100644 (file)
@@ -1,3 +1,8 @@
+import {
+  type ComputedRefImpl,
+  type ReactiveEffectRunner,
+  effect,
+} from '@vue/reactivity'
 import {
   type ComponentInternalInstance,
   type SetupContext,
@@ -25,8 +30,6 @@ 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', () => {
@@ -450,12 +453,12 @@ describe('SFC <script setup> helpers', () => {
       app.mount(root)
 
       await ready
-      expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
-      expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
+      expect(e!.effect.active).toBeTruthy()
+      expect(c!.flags & 1 /* SubscriberFlags.Tracking */).toBe(0)
 
       app.unmount()
-      expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
-      expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
+      expect(e!.effect.active).toBeFalsy()
+      expect(c!.flags & 1 /* SubscriberFlags.Tracking */).toBe(0)
     })
   })
 })
index 0cd3efa588c410989ace91a9dbfdb1d11ecac0f4..e31a7539d75aaa6987535c63abdb245a430d1e05 100644 (file)
@@ -531,6 +531,9 @@ describe('error handling', () => {
       caughtError = caught
     }
     expect(fn).toHaveBeenCalledWith(err, 'setup function')
+    expect(
+      `Active effect was not restored correctly - this is likely a Vue internal bug.`,
+    ).toHaveBeenWarned()
     expect(
       `Unhandled error during execution of setup function`,
     ).toHaveBeenWarned()
index 90cc22f547032c5a830be2fdda6bc1b96fceeeb3..55492a1bd5bcfc77c7ffac154f31d6528148b601 100644 (file)
@@ -1558,7 +1558,8 @@ function baseCreateRenderer(
     instance.scope.off()
 
     const update = (instance.update = effect.run.bind(effect))
-    const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
+    const job: SchedulerJob = (instance.job = () =>
+      effect.dirty && effect.run())
     job.i = instance
     job.id = instance.uid
     effect.scheduler = () => queueJob(job)
index 67db1d8506cec179264ed96e87fb8d6a0148c9e0..e2baf18158ed2cfd28c2308a36d006ad594537e7 100644 (file)
@@ -3,7 +3,7 @@ import { entries } from './scripts/aliases.js'
 
 export default defineConfig({
   define: {
-    __DEV__: true,
+    __DEV__: process.env.MODE !== 'benchmark',
     __TEST__: true,
     __VERSION__: '"test"',
     __BROWSER__: false,
@@ -24,6 +24,11 @@ export default defineConfig({
   test: {
     globals: true,
     pool: 'threads',
+    poolOptions: {
+      forks: {
+        execArgv: ['--expose-gc'],
+      },
+    },
     setupFiles: 'scripts/setup-vitest.ts',
     environmentMatchGlobs: [
       ['packages/{vue,vue-compat,runtime-dom}/**', 'jsdom'],