]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(reactivity, runtime-core): improve performance-critical code (#13274)
authorJohnson Chu <johnsoncodehk@gmail.com>
Thu, 10 Jul 2025 00:56:19 +0000 (08:56 +0800)
committerGitHub <noreply@github.com>
Thu, 10 Jul 2025 00:56:19 +0000 (17:56 -0700)
45 files changed:
packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/__tests__/effect.spec.ts
packages/reactivity/__tests__/effectScope.spec.ts
packages/reactivity/__tests__/watch.spec.ts
packages/reactivity/src/arrayInstrumentations.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/debug.ts
packages/reactivity/src/dep.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/effectScope.ts
packages/reactivity/src/index.ts
packages/reactivity/src/ref.ts
packages/reactivity/src/system.ts
packages/reactivity/src/watch.ts
packages/runtime-core/__tests__/apiWatch.spec.ts
packages/runtime-core/__tests__/scheduler.spec.ts
packages/runtime-core/src/apiLifecycle.ts
packages/runtime-core/src/apiSetupHelpers.ts
packages/runtime-core/src/apiWatch.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentCurrentInstance.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/components/KeepAlive.ts
packages/runtime-core/src/components/Suspense.ts
packages/runtime-core/src/components/Teleport.ts
packages/runtime-core/src/customFormatter.ts
packages/runtime-core/src/directives.ts
packages/runtime-core/src/errorHandling.ts
packages/runtime-core/src/hmr.ts
packages/runtime-core/src/hydration.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-core/src/rendererTemplateRef.ts
packages/runtime-core/src/scheduler.ts
packages/runtime-core/src/warning.ts
packages/runtime-vapor/__tests__/apiWatch.spec.ts
packages/runtime-vapor/__tests__/component.spec.ts
packages/runtime-vapor/__tests__/dom/prop.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/apiTemplateRef.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts
packages/runtime-vapor/src/hmr.ts
packages/runtime-vapor/src/renderEffect.ts

index 53f701d5ca901699d6d9cc5de4991c6dea4f65ae..84a2ccb2edcbbf6fee2f5ccd046da767910e1073 100644 (file)
@@ -27,7 +27,7 @@ import {
 } from '../src'
 import type { ComputedRef, ComputedRefImpl } from '../src/computed'
 import { pauseTracking, resetTracking } from '../src/effect'
-import { SubscriberFlags } from '../src/system'
+import { ReactiveFlags } from '../src/system'
 
 describe('reactivity/computed', () => {
   it('should return updated value', () => {
@@ -467,12 +467,8 @@ describe('reactivity/computed', () => {
     const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
 
     c2.value
-    expect(
-      c1.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
-    ).toBe(0)
-    expect(
-      c2.flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed),
-    ).toBe(0)
+    expect(c1.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0)
+    expect(c2.flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)).toBe(0)
   })
 
   it('should chained computeds dirtyLevel update with first computed effect', () => {
index 20f0244a7bc8fd3d3d4860298fd72119da97bb1d..5d7a1e39cef5163bae8f05602e1828fa00171098 100644 (file)
@@ -22,7 +22,7 @@ import {
   stop,
   toRaw,
 } from '../src/index'
-import { type Dependency, endBatch, startBatch } from '../src/system'
+import { type ReactiveNode, endBatch, startBatch } from '../src/system'
 
 describe('reactivity/effect', () => {
   it('should run the passed function once (wrapped by a effect)', () => {
@@ -1178,7 +1178,7 @@ describe('reactivity/effect', () => {
   })
 
   describe('dep unsubscribe', () => {
-    function getSubCount(dep: Dependency | undefined) {
+    function getSubCount(dep: ReactiveNode | undefined) {
       let count = 0
       let sub = dep!.subs
       while (sub) {
index 84310b985f25faf1fa3c86b99bc9b01329f4bf81..93ee648e2df0d732b38b6f6a0dd5919b0b7ad2f5 100644 (file)
@@ -2,6 +2,7 @@ import { nextTick, watch, watchEffect } from '@vue/runtime-core'
 import {
   type ComputedRef,
   EffectScope,
+  ReactiveEffect,
   computed,
   effect,
   effectScope,
@@ -9,6 +10,7 @@ import {
   onScopeDispose,
   reactive,
   ref,
+  setCurrentScope,
 } from '../src'
 
 describe('reactivity/effect/scope', () => {
@@ -20,7 +22,7 @@ describe('reactivity/effect/scope', () => {
 
   it('should accept zero argument', () => {
     const scope = effectScope()
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
   })
 
   it('should return run value', () => {
@@ -29,7 +31,8 @@ describe('reactivity/effect/scope', () => {
 
   it('should work w/ active property', () => {
     const scope = effectScope()
-    scope.run(() => 1)
+    const src = computed(() => 1)
+    scope.run(() => src.value)
     expect(scope.active).toBe(true)
     scope.stop()
     expect(scope.active).toBe(false)
@@ -47,7 +50,7 @@ describe('reactivity/effect/scope', () => {
       expect(dummy).toBe(7)
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
   })
 
   it('stop', () => {
@@ -60,7 +63,7 @@ describe('reactivity/effect/scope', () => {
       effect(() => (doubled = counter.num * 2))
     })
 
-    expect(scope.effects.length).toBe(2)
+    expect(getEffectsCount(scope)).toBe(2)
 
     expect(dummy).toBe(0)
     counter.num = 7
@@ -87,9 +90,8 @@ describe('reactivity/effect/scope', () => {
       })
     })
 
-    expect(scope.effects.length).toBe(1)
-    expect(scope.scopes!.length).toBe(1)
-    expect(scope.scopes![0]).toBeInstanceOf(EffectScope)
+    expect(getEffectsCount(scope)).toBe(1)
+    expect(scope.deps?.nextDep?.dep).toBeInstanceOf(EffectScope)
 
     expect(dummy).toBe(0)
     counter.num = 7
@@ -117,7 +119,7 @@ describe('reactivity/effect/scope', () => {
       })
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
 
     expect(dummy).toBe(0)
     counter.num = 7
@@ -142,13 +144,13 @@ describe('reactivity/effect/scope', () => {
       effect(() => (dummy = counter.num))
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
 
     scope.run(() => {
       effect(() => (doubled = counter.num * 2))
     })
 
-    expect(scope.effects.length).toBe(2)
+    expect(getEffectsCount(scope)).toBe(2)
 
     counter.num = 7
     expect(dummy).toBe(7)
@@ -166,21 +168,21 @@ describe('reactivity/effect/scope', () => {
       effect(() => (dummy = counter.num))
     })
 
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
 
     scope.stop()
 
+    expect(getEffectsCount(scope)).toBe(0)
+
     scope.run(() => {
       effect(() => (doubled = counter.num * 2))
     })
 
-    expect('[Vue warn] cannot run an inactive effect scope.').toHaveBeenWarned()
-
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(1)
 
     counter.num = 7
     expect(dummy).toBe(0)
-    expect(doubled).toBe(undefined)
+    expect(doubled).toBe(14)
   })
 
   it('should fire onScopeDispose hook', () => {
@@ -224,9 +226,9 @@ describe('reactivity/effect/scope', () => {
   it('should dereference child scope from parent scope after stopping child scope (no memleaks)', () => {
     const parent = effectScope()
     const child = parent.run(() => effectScope())!
-    expect(parent.scopes!.includes(child)).toBe(true)
+    expect(parent.deps?.dep).toBe(child)
     child.stop()
-    expect(parent.scopes!.includes(child)).toBe(false)
+    expect(parent.deps).toBeUndefined()
   })
 
   it('test with higher level APIs', async () => {
@@ -290,21 +292,7 @@ describe('reactivity/effect/scope', () => {
 
     parentScope.run(() => {
       const childScope = effectScope(true)
-      childScope.on()
-      childScope.off()
-      expect(getCurrentScope()).toBe(parentScope)
-    })
-  })
-
-  it('calling on() and off() multiple times inside an active scope should not break currentScope', () => {
-    const parentScope = effectScope()
-    parentScope.run(() => {
-      const childScope = effectScope(true)
-      childScope.on()
-      childScope.on()
-      childScope.off()
-      childScope.off()
-      childScope.off()
+      setCurrentScope(setCurrentScope(childScope))
       expect(getCurrentScope()).toBe(parentScope)
     })
   })
@@ -372,7 +360,17 @@ describe('reactivity/effect/scope', () => {
     expect(watcherCalls).toBe(3)
     expect(cleanupCalls).toBe(1)
 
-    expect(scope.effects.length).toBe(0)
-    expect(scope.cleanups.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
+    expect(scope.cleanupsLength).toBe(0)
   })
 })
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
index 9bec54e5f6859f070fb6bf35ea4b8739d092b7e9..e8686700aae5098bc57d62e6b5096ae89a08cdcc 100644 (file)
@@ -3,40 +3,12 @@ import {
   type Ref,
   WatchErrorCodes,
   type WatchOptions,
-  type WatchScheduler,
   computed,
   onWatcherCleanup,
   ref,
   watch,
 } from '../src'
 
-const queue: (() => void)[] = []
-
-// a simple scheduler for testing purposes
-let isFlushPending = false
-const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
-const nextTick = (fn?: () => any) =>
-  fn ? resolvedPromise.then(fn) : resolvedPromise
-
-const scheduler: WatchScheduler = (job, isFirstRun) => {
-  if (isFirstRun) {
-    job()
-  } else {
-    queue.push(job)
-    flushJobs()
-  }
-}
-
-const flushJobs = () => {
-  if (isFlushPending) return
-  isFlushPending = true
-  resolvedPromise.then(() => {
-    queue.forEach(job => job())
-    queue.length = 0
-    isFlushPending = false
-  })
-}
-
 describe('watch', () => {
   test('effect', () => {
     let dummy: any
@@ -147,54 +119,6 @@ describe('watch', () => {
     expect(dummy).toBe(30)
   })
 
-  test('nested calls to baseWatch and onWatcherCleanup', async () => {
-    let calls: string[] = []
-    let source: Ref<number>
-    let copyist: Ref<number>
-    const scope = new EffectScope()
-
-    scope.run(() => {
-      source = ref(0)
-      copyist = ref(0)
-      // sync by default
-      watch(
-        () => {
-          const current = (copyist.value = source.value)
-          onWatcherCleanup(() => calls.push(`sync ${current}`))
-        },
-        null,
-        {},
-      )
-      // with scheduler
-      watch(
-        () => {
-          const current = copyist.value
-          onWatcherCleanup(() => calls.push(`post ${current}`))
-        },
-        null,
-        { scheduler },
-      )
-    })
-
-    await nextTick()
-    expect(calls).toEqual([])
-
-    scope.run(() => source.value++)
-    expect(calls).toEqual(['sync 0'])
-    await nextTick()
-    expect(calls).toEqual(['sync 0', 'post 0'])
-    calls.length = 0
-
-    scope.run(() => source.value++)
-    expect(calls).toEqual(['sync 1'])
-    await nextTick()
-    expect(calls).toEqual(['sync 1', 'post 1'])
-    calls.length = 0
-
-    scope.stop()
-    expect(calls).toEqual(['sync 2', 'post 2'])
-  })
-
   test('once option should be ignored by simple watch', async () => {
     let dummy: any
     const source = ref(0)
index 8d578c7d860d62304783a629165392a609d91d4f..5e35230efdf4eb74143968b1e3db2278937f0b08 100644 (file)
@@ -1,9 +1,8 @@
 import { isArray } from '@vue/shared'
 import { TrackOpTypes } from './constants'
 import { ARRAY_ITERATE_KEY, track } from './dep'
-import { pauseTracking, resetTracking } from './effect'
 import { isProxy, isShallow, toRaw, toReactive } from './reactive'
-import { endBatch, startBatch } from './system'
+import { endBatch, setActiveSub, startBatch } from './system'
 
 /**
  * Track array iteration and return:
@@ -320,10 +319,10 @@ function noTracking(
   method: keyof Array<any>,
   args: unknown[] = [],
 ) {
-  pauseTracking()
   startBatch()
+  const prevSub = setActiveSub()
   const res = (toRaw(self) as any)[method].apply(self, args)
+  setActiveSub(prevSub)
   endBatch()
-  resetTracking()
   return res
 }
index ad518f3c5e6cc3cab956f99853719a1cba92c9da..cb367a274b3ebcce0c72e14e6202b7cc3a1afbad 100644 (file)
@@ -1,24 +1,19 @@
 import { hasChanged, isFunction } from '@vue/shared'
 import { ReactiveFlags, TrackOpTypes } from './constants'
 import { onTrack, setupOnTrigger } from './debug'
-import {
-  type DebuggerEvent,
-  type DebuggerOptions,
-  activeSub,
-  setActiveSub,
-} from './effect'
+import type { DebuggerEvent, DebuggerOptions } from './effect'
 import { activeEffectScope } from './effectScope'
 import type { Ref } from './ref'
 import {
-  type Dependency,
   type Link,
-  type Subscriber,
-  SubscriberFlags,
+  type ReactiveNode,
+  ReactiveFlags as SystemReactiveFlags,
+  activeSub,
+  checkDirty,
   endTracking,
   link,
-  processComputedUpdate,
+  shallowPropagate,
   startTracking,
-  updateDirtyFlag,
 } from './system'
 import { warn } from './warning'
 
@@ -53,20 +48,18 @@ 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 Dependency, Subscriber {
+export class ComputedRefImpl<T = any> implements ReactiveNode {
   /**
    * @internal
    */
   _value: T | undefined = undefined
 
-  // Dependency
   subs: Link | undefined = undefined
   subsTail: Link | undefined = undefined
-
-  // Subscriber
   deps: Link | undefined = undefined
   depsTail: Link | undefined = undefined
-  flags: SubscriberFlags = SubscriberFlags.Computed | SubscriberFlags.Dirty
+  flags: SystemReactiveFlags =
+    SystemReactiveFlags.Mutable | SystemReactiveFlags.Dirty
 
   /**
    * @internal
@@ -84,7 +77,7 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
     return this
   }
   // for backwards compat
-  get dep(): Dependency {
+  get dep(): ReactiveNode {
     return this
   }
   /**
@@ -93,13 +86,17 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
    */
   get _dirty(): boolean {
     const flags = this.flags
-    if (
-      flags & SubscriberFlags.Dirty ||
-      (flags & SubscriberFlags.PendingComputed &&
-        updateDirtyFlag(this, this.flags))
-    ) {
+    if (flags & SystemReactiveFlags.Dirty) {
       return true
     }
+    if (flags & SystemReactiveFlags.Pending) {
+      if (checkDirty(this.deps!, this)) {
+        this.flags = flags | SystemReactiveFlags.Dirty
+        return true
+      } else {
+        this.flags = flags & ~SystemReactiveFlags.Pending
+      }
+    }
     return false
   }
   /**
@@ -108,9 +105,9 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
    */
   set _dirty(v: boolean) {
     if (v) {
-      this.flags |= SubscriberFlags.Dirty
+      this.flags |= SystemReactiveFlags.Dirty
     } else {
-      this.flags &= ~(SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)
+      this.flags &= ~(SystemReactiveFlags.Dirty | SystemReactiveFlags.Pending)
     }
   }
 
@@ -128,8 +125,18 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
 
   get value(): T {
     const flags = this.flags
-    if (flags & (SubscriberFlags.Dirty | SubscriberFlags.PendingComputed)) {
-      processComputedUpdate(this, flags)
+    if (
+      flags & SystemReactiveFlags.Dirty ||
+      (flags & SystemReactiveFlags.Pending && checkDirty(this.deps!, this))
+    ) {
+      if (this.update()) {
+        const subs = this.subs
+        if (subs !== undefined) {
+          shallowPropagate(subs)
+        }
+      }
+    } else if (flags & SystemReactiveFlags.Pending) {
+      this.flags = flags & ~SystemReactiveFlags.Pending
     }
     if (activeSub !== undefined) {
       if (__DEV__) {
@@ -155,9 +162,7 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
   }
 
   update(): boolean {
-    const prevSub = activeSub
-    setActiveSub(this)
-    startTracking(this)
+    const prevSub = startTracking(this)
     try {
       const oldValue = this._value
       const newValue = this.fn(oldValue)
@@ -167,8 +172,7 @@ export class ComputedRefImpl<T = any> implements Dependency, Subscriber {
       }
       return false
     } finally {
-      setActiveSub(prevSub)
-      endTracking(this)
+      endTracking(this, prevSub)
     }
   }
 }
index 5503dc8a11b763d9cb7e81829d846cb94e9ba9e1..ba323d8993ca8411f5729294748f7bbb018ed1cd 100644 (file)
@@ -1,6 +1,6 @@
 import { extend } from '@vue/shared'
 import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect'
-import { type Link, type Subscriber, SubscriberFlags } from './system'
+import { type Link, ReactiveFlags, type ReactiveNode } from './system'
 
 export const triggerEventInfos: DebuggerEventExtraInfo[] = []
 
@@ -61,7 +61,7 @@ export function setupOnTrigger(target: { new (...args: any[]): any }): void {
   })
 }
 
-function setupFlagsHandler(target: Subscriber): void {
+function setupFlagsHandler(target: ReactiveNode): void {
   ;(target as any)._flags = target.flags
   Object.defineProperty(target, 'flags', {
     get() {
@@ -69,8 +69,11 @@ function setupFlagsHandler(target: Subscriber): void {
     },
     set(value) {
       if (
-        !((target as any)._flags & SubscriberFlags.Propagated) &&
-        !!(value & SubscriberFlags.Propagated)
+        !(
+          (target as any)._flags &
+          (ReactiveFlags.Dirty | ReactiveFlags.Pending)
+        ) &&
+        !!(value & (ReactiveFlags.Dirty | ReactiveFlags.Pending))
       ) {
         onTrigger(this)
       }
index 184964c17b8847b9a18af0c03f83056407c4230b..a24000329c1c5dbfdbafd61802911635b266061e 100644 (file)
@@ -1,19 +1,22 @@
 import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
 import { type TrackOpTypes, TriggerOpTypes } from './constants'
 import { onTrack, triggerEventInfos } from './debug'
-import { activeSub } from './effect'
 import {
-  type Dependency,
   type Link,
+  ReactiveFlags,
+  type ReactiveNode,
+  activeSub,
   endBatch,
   link,
   propagate,
+  shallowPropagate,
   startBatch,
 } from './system'
 
-class Dep implements Dependency {
+class Dep implements ReactiveNode {
   _subs: Link | undefined = undefined
   subsTail: Link | undefined = undefined
+  flags: ReactiveFlags = ReactiveFlags.None
 
   constructor(
     private map: KeyToDepMap,
@@ -103,7 +106,7 @@ export function trigger(
     return
   }
 
-  const run = (dep: Dependency | undefined) => {
+  const run = (dep: ReactiveNode | undefined) => {
     if (dep !== undefined && dep.subs !== undefined) {
       if (__DEV__) {
         triggerEventInfos.push({
@@ -116,6 +119,7 @@ export function trigger(
         })
       }
       propagate(dep.subs)
+      shallowPropagate(dep.subs)
       if (__DEV__) {
         triggerEventInfos.pop()
       }
@@ -190,7 +194,7 @@ export function trigger(
 export function getDepFromReactive(
   object: any,
   key: string | number | symbol,
-): Dependency | undefined {
+): ReactiveNode | undefined {
   const depMap = targetMap.get(object)
   return depMap && depMap.get(key)
 }
index a77c4bf2b18e49b4dd5cde3c98127f1c4e5e0878..af8ebea89a128e6d135b273fd17af0b72aba3ccd 100644 (file)
@@ -4,18 +4,22 @@ import { setupOnTrigger } from './debug'
 import { activeEffectScope } from './effectScope'
 import {
   type Link,
-  type Subscriber,
-  SubscriberFlags,
+  ReactiveFlags,
+  type ReactiveNode,
+  activeSub,
+  checkDirty,
   endTracking,
+  link,
+  setActiveSub,
   startTracking,
-  updateDirtyFlag,
+  unlink,
 } from './system'
 import { warn } from './warning'
 
 export type EffectScheduler = (...args: any[]) => any
 
 export type DebuggerEvent = {
-  effect: Subscriber
+  effect: ReactiveNode
 } & DebuggerEventExtraInfo
 
 export type DebuggerEventExtraInfo = {
@@ -48,118 +52,111 @@ export enum EffectFlags {
    */
   ALLOW_RECURSE = 1 << 7,
   PAUSED = 1 << 8,
-  NOTIFIED = 1 << 9,
-  STOP = 1 << 10,
 }
 
-export class ReactiveEffect<T = any> implements ReactiveEffectOptions {
-  // Subscriber
+export class ReactiveEffect<T = any>
+  implements ReactiveEffectOptions, ReactiveNode
+{
   deps: Link | undefined = undefined
   depsTail: Link | undefined = undefined
-  flags: number = SubscriberFlags.Effect
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
+  flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty
 
   /**
    * @internal
    */
-  cleanup?: () => void = undefined
+  cleanups: (() => void)[] = []
+  /**
+   * @internal
+   */
+  cleanupsLength = 0
 
-  onStop?: () => void
+  // dev only
   onTrack?: (event: DebuggerEvent) => void
+  // dev only
   onTrigger?: (event: DebuggerEvent) => void
 
-  constructor(public fn: () => T) {
-    if (activeEffectScope && activeEffectScope.active) {
-      activeEffectScope.effects.push(this)
+  // @ts-expect-error
+  fn(): T {}
+
+  constructor(fn?: () => T) {
+    if (fn !== undefined) {
+      this.fn = fn
+    }
+    if (activeEffectScope) {
+      link(this, activeEffectScope)
     }
   }
 
   get active(): boolean {
-    return !(this.flags & EffectFlags.STOP)
+    return !!this.flags || this.deps !== undefined
   }
 
   pause(): void {
-    if (!(this.flags & EffectFlags.PAUSED)) {
-      this.flags |= EffectFlags.PAUSED
-    }
+    this.flags |= EffectFlags.PAUSED
   }
 
   resume(): void {
-    const flags = this.flags
-    if (flags & EffectFlags.PAUSED) {
-      this.flags &= ~EffectFlags.PAUSED
-    }
-    if (flags & EffectFlags.NOTIFIED) {
-      this.flags &= ~EffectFlags.NOTIFIED
+    const flags = (this.flags &= ~EffectFlags.PAUSED)
+    if (flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) {
       this.notify()
     }
   }
 
   notify(): void {
-    const flags = this.flags
-    if (!(flags & EffectFlags.PAUSED)) {
-      this.scheduler()
-    } else {
-      this.flags |= EffectFlags.NOTIFIED
-    }
-  }
-
-  scheduler(): void {
-    if (this.dirty) {
+    if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
       this.run()
     }
   }
 
   run(): T {
-    // TODO cleanupEffect
-
     if (!this.active) {
-      // stopped during cleanup
       return this.fn()
     }
-    cleanupEffect(this)
-    const prevSub = activeSub
-    setActiveSub(this)
-    startTracking(this)
-
+    cleanup(this)
+    const prevSub = startTracking(this)
     try {
       return this.fn()
     } finally {
-      if (__DEV__ && activeSub !== this) {
-        warn(
-          'Active effect was not restored correctly - ' +
-            'this is likely a Vue internal bug.',
-        )
-      }
-      setActiveSub(prevSub)
-      endTracking(this)
+      endTracking(this, prevSub)
+      const flags = this.flags
       if (
-        this.flags & SubscriberFlags.Recursed &&
-        this.flags & EffectFlags.ALLOW_RECURSE
+        (flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
+        (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
       ) {
-        this.flags &= ~SubscriberFlags.Recursed
+        this.flags = flags & ~ReactiveFlags.Recursed
         this.notify()
       }
     }
   }
 
   stop(): void {
-    if (this.active) {
-      startTracking(this)
-      endTracking(this)
-      cleanupEffect(this)
-      this.onStop && this.onStop()
-      this.flags |= EffectFlags.STOP
+    let dep = this.deps
+    while (dep !== undefined) {
+      dep = unlink(dep, this)
+    }
+    const sub = this.subs
+    if (sub !== undefined) {
+      unlink(sub)
     }
+    this.flags = 0
+    cleanup(this)
   }
 
   get dirty(): boolean {
     const flags = this.flags
-    if (
-      flags & SubscriberFlags.Dirty ||
-      (flags & SubscriberFlags.PendingComputed && updateDirtyFlag(this, flags))
-    ) {
+    if (flags & ReactiveFlags.Dirty) {
       return true
     }
+    if (flags & ReactiveFlags.Pending) {
+      if (checkDirty(this.deps!, this)) {
+        this.flags = flags | ReactiveFlags.Dirty
+        return true
+      } else {
+        this.flags = flags & ~ReactiveFlags.Pending
+      }
+    }
     return false
   }
 }
@@ -183,6 +180,23 @@ export function effect<T = any>(
 
   const e = new ReactiveEffect(fn)
   if (options) {
+    const { onStop, scheduler } = options
+    if (onStop) {
+      options.onStop = undefined
+      const stop = e.stop.bind(e)
+      e.stop = () => {
+        stop()
+        onStop()
+      }
+    }
+    if (scheduler) {
+      options.scheduler = undefined
+      e.notify = () => {
+        if (!(e.flags & EffectFlags.PAUSED)) {
+          scheduler()
+        }
+      }
+    }
     extend(e, options)
   }
   try {
@@ -205,14 +219,14 @@ export function stop(runner: ReactiveEffectRunner): void {
   runner.effect.stop()
 }
 
-const resetTrackingStack: (Subscriber | undefined)[] = []
+const resetTrackingStack: (ReactiveNode | undefined)[] = []
 
 /**
  * Temporarily pauses tracking.
  */
 export function pauseTracking(): void {
   resetTrackingStack.push(activeSub)
-  activeSub = undefined
+  setActiveSub()
 }
 
 /**
@@ -230,7 +244,7 @@ export function enableTracking(): void {
     resetTrackingStack.push(undefined)
     for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
       if (resetTrackingStack[i] !== undefined) {
-        activeSub = resetTrackingStack[i]
+        setActiveSub(resetTrackingStack[i])
         break
       }
     }
@@ -248,9 +262,21 @@ export function resetTracking(): void {
     )
   }
   if (resetTrackingStack.length) {
-    activeSub = resetTrackingStack.pop()!
+    setActiveSub(resetTrackingStack.pop()!)
   } else {
-    activeSub = undefined
+    setActiveSub()
+  }
+}
+
+export function cleanup(
+  sub: ReactiveNode & { cleanups: (() => void)[]; cleanupsLength: number },
+): void {
+  const l = sub.cleanupsLength
+  if (l) {
+    for (let i = 0; i < l; i++) {
+      sub.cleanups[i]()
+    }
+    sub.cleanupsLength = 0
   }
 }
 
@@ -268,7 +294,7 @@ export function resetTracking(): void {
  */
 export function onEffectCleanup(fn: () => void, failSilently = false): void {
   if (activeSub instanceof ReactiveEffect) {
-    activeSub.cleanup = fn
+    activeSub.cleanups[activeSub.cleanupsLength++] = () => cleanupEffect(fn)
   } else if (__DEV__ && !failSilently) {
     warn(
       `onEffectCleanup() was called when there was no active effect` +
@@ -277,23 +303,12 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
   }
 }
 
-function cleanupEffect(e: ReactiveEffect) {
-  const { cleanup } = e
-  e.cleanup = undefined
-  if (cleanup !== undefined) {
-    // run cleanup without active effect
-    const prevSub = activeSub
-    activeSub = undefined
-    try {
-      cleanup()
-    } finally {
-      activeSub = prevSub
-    }
+function cleanupEffect(fn: () => void) {
+  // run cleanup without active effect
+  const prevSub = setActiveSub()
+  try {
+    fn()
+  } finally {
+    setActiveSub(prevSub)
   }
 }
-
-export let activeSub: Subscriber | undefined = undefined
-
-export function setActiveSub(sub: Subscriber | undefined): void {
-  activeSub = sub
-}
index 00fa403b02e184603cf2ddc35cce93031acad3e6..819eb1ef73b4ebf76366dda012805e298316c5f2 100644 (file)
@@ -1,76 +1,50 @@
-import { EffectFlags, type ReactiveEffect } from './effect'
+import { EffectFlags, cleanup } from './effect'
 import {
   type Link,
-  type Subscriber,
-  endTracking,
-  startTracking,
+  type ReactiveNode,
+  link,
+  setActiveSub,
+  unlink,
 } from './system'
 import { warn } from './warning'
 
 export let activeEffectScope: EffectScope | undefined
 
-export class EffectScope implements Subscriber {
-  // Subscriber: In order to collect orphans computeds
+export class EffectScope implements ReactiveNode {
   deps: Link | undefined = undefined
   depsTail: Link | undefined = undefined
+  subs: Link | undefined = undefined
+  subsTail: Link | undefined = undefined
   flags: number = 0
 
-  /**
-   * @internal track `on` calls, allow `on` call multiple times
-   */
-  private _on = 0
-  /**
-   * @internal
-   */
-  effects: ReactiveEffect[] = []
   /**
    * @internal
    */
   cleanups: (() => void)[] = []
-
-  /**
-   * only assigned by undetached scope
-   * @internal
-   */
-  parent: EffectScope | undefined
-  /**
-   * record undetached scopes
-   * @internal
-   */
-  scopes: EffectScope[] | undefined
   /**
-   * track a child scope's index in its parent's scopes array for optimized
-   * removal
    * @internal
    */
-  private index: number | undefined
+  cleanupsLength = 0
 
-  constructor(
-    public detached = false,
-    parent: EffectScope | undefined = activeEffectScope,
-  ) {
-    this.parent = parent
-    if (!detached && parent) {
-      this.index = (parent.scopes || (parent.scopes = [])).push(this) - 1
+  constructor(detached = false) {
+    if (!detached && activeEffectScope) {
+      link(this, activeEffectScope)
     }
   }
 
   get active(): boolean {
-    return !(this.flags & EffectFlags.STOP)
+    return !!this.flags || this.deps !== undefined
   }
 
   pause(): void {
     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].pause()
+      for (let link = this.deps; link !== undefined; link = link.nextDep) {
+        const dep = link.dep
+        if ('pause' in dep) {
+          dep.pause()
         }
       }
-      for (i = 0, l = this.effects.length; i < l; i++) {
-        this.effects[i].pause()
-      }
     }
   }
 
@@ -78,91 +52,47 @@ export class EffectScope implements Subscriber {
    * Resumes the effect scope, including all child scopes and effects.
    */
   resume(): void {
-    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()
+    const flags = this.flags
+    if (flags & EffectFlags.PAUSED) {
+      this.flags = flags & ~EffectFlags.PAUSED
+      for (let link = this.deps; link !== undefined; link = link.nextDep) {
+        const dep = link.dep
+        if ('resume' in dep) {
+          dep.resume()
         }
       }
-      for (i = 0, l = this.effects.length; i < l; i++) {
-        this.effects[i].resume()
-      }
     }
   }
 
   run<T>(fn: () => T): T | undefined {
-    if (this.active) {
-      const prevEffectScope = activeEffectScope
-      try {
-        activeEffectScope = this
-        return fn()
-      } finally {
-        activeEffectScope = prevEffectScope
-      }
-    } else if (__DEV__) {
-      warn(`cannot run an inactive effect scope.`)
-    }
-  }
-
-  prevScope: EffectScope | undefined
-  /**
-   * This should only be called on non-detached scopes
-   * @internal
-   */
-  on(): void {
-    if (++this._on === 1) {
-      this.prevScope = activeEffectScope
+    const prevSub = setActiveSub()
+    const prevScope = activeEffectScope
+    try {
       activeEffectScope = this
+      return fn()
+    } finally {
+      activeEffectScope = prevScope
+      setActiveSub(prevSub)
     }
   }
 
-  /**
-   * This should only be called on non-detached scopes
-   * @internal
-   */
-  off(): void {
-    if (this._on > 0 && --this._on === 0) {
-      activeEffectScope = this.prevScope
-      this.prevScope = undefined
-    }
-  }
-
-  stop(fromParent?: boolean): void {
-    if (this.active) {
-      this.flags |= EffectFlags.STOP
-      startTracking(this)
-      endTracking(this)
-      let i, l
-      for (i = 0, l = this.effects.length; i < l; i++) {
-        this.effects[i].stop()
+  stop(): void {
+    let dep = this.deps
+    while (dep !== undefined) {
+      const node = dep.dep
+      if ('stop' in node) {
+        dep = dep.nextDep
+        node.stop()
+      } else {
+        dep = unlink(dep, this)
       }
-      this.effects.length = 0
-
-      for (i = 0, l = this.cleanups.length; i < l; i++) {
-        this.cleanups[i]()
-      }
-      this.cleanups.length = 0
-
-      if (this.scopes) {
-        for (i = 0, l = this.scopes.length; i < l; i++) {
-          this.scopes[i].stop(true)
-        }
-        this.scopes.length = 0
-      }
-
-      // nested scope, dereference from parent to avoid memory leaks
-      if (!this.detached && this.parent && !fromParent) {
-        // optimized O(1) removal
-        const last = this.parent.scopes!.pop()
-        if (last && last !== this) {
-          this.parent.scopes![this.index!] = last
-          last.index = this.index!
-        }
-      }
-      this.parent = undefined
     }
+    const sub = this.subs
+    if (sub !== undefined) {
+      unlink(sub)
+    }
+    this.flags = 0
+    cleanup(this)
   }
 }
 
@@ -188,6 +118,14 @@ export function getCurrentScope(): EffectScope | undefined {
   return activeEffectScope
 }
 
+export function setCurrentScope(scope?: EffectScope): EffectScope | undefined {
+  try {
+    return activeEffectScope
+  } finally {
+    activeEffectScope = scope
+  }
+}
+
 /**
  * Registers a dispose callback on the current active effect scope. The
  * callback will be invoked when the associated effect scope is stopped.
@@ -196,8 +134,8 @@ export function getCurrentScope(): EffectScope | undefined {
  * @see {@link https://vuejs.org/api/reactivity-advanced.html#onscopedispose}
  */
 export function onScopeDispose(fn: () => void, failSilently = false): void {
-  if (activeEffectScope) {
-    activeEffectScope.cleanups.push(fn)
+  if (activeEffectScope !== undefined) {
+    activeEffectScope.cleanups[activeEffectScope.cleanupsLength++] = fn
   } else if (__DEV__ && !failSilently) {
     warn(
       `onScopeDispose() is called when there is no active effect scope` +
index f0445e87da0dcc0538b2ad63a88822aa4cdd02bc..ef643940b00d53b36568270966ddfdffa81dc290 100644 (file)
@@ -76,6 +76,10 @@ export {
   effectScope,
   EffectScope,
   getCurrentScope,
+  /**
+   * @internal
+   */
+  setCurrentScope,
   onScopeDispose,
 } from './effectScope'
 export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
@@ -86,8 +90,11 @@ export {
   traverse,
   onWatcherCleanup,
   WatchErrorCodes,
+  /**
+   * @internal
+   */
+  WatcherEffect,
   type WatchOptions,
-  type WatchScheduler,
   type WatchStopHandle,
   type WatchHandle,
   type WatchEffect,
@@ -95,3 +102,7 @@ export {
   type WatchCallback,
   type OnCleanup,
 } from './watch'
+/**
+ * @internal
+ */
+export { setActiveSub } from './system'
index 5239f34bf3fc1a80d9118343ec7de332fe5fd6f0..92a4ba7f1de6c156e68b91d444d2b26e94077306 100644 (file)
@@ -9,7 +9,6 @@ import type { ComputedRef, WritableComputedRef } from './computed'
 import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
 import { onTrack, triggerEventInfos } from './debug'
 import { getDepFromReactive } from './dep'
-import { activeSub } from './effect'
 import {
   type Builtin,
   type ShallowReactiveMarker,
@@ -19,7 +18,17 @@ import {
   toRaw,
   toReactive,
 } from './reactive'
-import { type Dependency, type Link, link, propagate } from './system'
+import {
+  type Link,
+  type ReactiveNode,
+  ReactiveFlags as _ReactiveFlags,
+  activeSub,
+  batchDepth,
+  flush,
+  link,
+  propagate,
+  shallowPropagate,
+} from './system'
 
 declare const RefSymbol: unique symbol
 export declare const RawSymbol: unique symbol
@@ -106,31 +115,46 @@ function createRef(rawValue: unknown, wrap?: <T>(v: T) => T) {
 /**
  * @internal
  */
-class RefImpl<T = any> implements Dependency {
-  // Dependency
+class RefImpl<T = any> implements ReactiveNode {
   subs: Link | undefined = undefined
   subsTail: Link | undefined = undefined
+  flags: _ReactiveFlags = _ReactiveFlags.Mutable
 
   _value: T
   _wrap?: <T>(v: T) => T
+  private _oldValue: T
   private _rawValue: T
 
-  public readonly [ReactiveFlags.IS_REF] = true
-  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
+  /**
+   * @internal
+   */
+  readonly __v_isRef = true
+  // TODO isolatedDeclarations ReactiveFlags.IS_REF
+  /**
+   * @internal
+   */
+  readonly __v_isShallow: boolean = false
+  // TODO isolatedDeclarations ReactiveFlags.IS_SHALLOW
 
   constructor(value: T, wrap: (<T>(v: T) => T) | undefined) {
-    this._rawValue = wrap ? toRaw(value) : value
+    this._oldValue = this._rawValue = wrap ? toRaw(value) : value
     this._value = wrap ? wrap(value) : value
     this._wrap = wrap
     this[ReactiveFlags.IS_SHALLOW] = !wrap
   }
 
-  get dep() {
+  get dep(): this {
     return this
   }
 
-  get value() {
+  get value(): T {
     trackRef(this)
+    if (this.flags & _ReactiveFlags.Dirty && this.update()) {
+      const subs = this.subs
+      if (subs !== undefined) {
+        shallowPropagate(subs)
+      }
+    }
     return this._value
   }
 
@@ -142,24 +166,36 @@ class RefImpl<T = any> implements Dependency {
       isReadonly(newValue)
     newValue = useDirectValue ? newValue : toRaw(newValue)
     if (hasChanged(newValue, oldValue)) {
+      this.flags |= _ReactiveFlags.Dirty
       this._rawValue = newValue
       this._value =
-        this._wrap && !useDirectValue ? this._wrap(newValue) : newValue
-      if (__DEV__) {
-        triggerEventInfos.push({
-          target: this,
-          type: TriggerOpTypes.SET,
-          key: 'value',
-          newValue,
-          oldValue,
-        })
-      }
-      triggerRef(this as unknown as Ref)
-      if (__DEV__) {
-        triggerEventInfos.pop()
+        !useDirectValue && this._wrap ? this._wrap(newValue) : newValue
+      const subs = this.subs
+      if (subs !== undefined) {
+        if (__DEV__) {
+          triggerEventInfos.push({
+            target: this,
+            type: TriggerOpTypes.SET,
+            key: 'value',
+            newValue,
+            oldValue,
+          })
+        }
+        propagate(subs)
+        if (!batchDepth) {
+          flush()
+        }
+        if (__DEV__) {
+          triggerEventInfos.pop()
+        }
       }
     }
   }
+
+  update(): boolean {
+    this.flags &= ~_ReactiveFlags.Dirty
+    return hasChanged(this._oldValue, (this._oldValue = this._rawValue))
+  }
 }
 
 /**
@@ -192,10 +228,14 @@ export function triggerRef(ref: Ref): void {
   const dep = (ref as unknown as RefImpl).dep
   if (dep !== undefined && dep.subs !== undefined) {
     propagate(dep.subs)
+    shallowPropagate(dep.subs)
+    if (!batchDepth) {
+      flush()
+    }
   }
 }
 
-function trackRef(dep: Dependency) {
+function trackRef(dep: ReactiveNode) {
   if (activeSub !== undefined) {
     if (__DEV__) {
       onTrack(activeSub!, {
@@ -296,10 +336,10 @@ export type CustomRefFactory<T> = (
   set: (value: T) => void
 }
 
-class CustomRefImpl<T> implements Dependency {
-  // Dependency
+class CustomRefImpl<T> implements ReactiveNode {
   subs: Link | undefined = undefined
   subsTail: Link | undefined = undefined
+  flags: _ReactiveFlags = _ReactiveFlags.None
 
   private readonly _get: ReturnType<CustomRefFactory<T>>['get']
   private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@@ -380,7 +420,7 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
     this._object[this._key] = newVal
   }
 
-  get dep(): Dependency | undefined {
+  get dep(): ReactiveNode | undefined {
     return getDepFromReactive(toRaw(this._object), this._key)
   }
 }
index b3699f727c87751b3bf76554994fb2daa6e27590..cc3eaad45651bfd72efcfbace6cdcf1d25e53d41 100644 (file)
 /* eslint-disable */
-// Ported from https://github.com/stackblitz/alien-signals/blob/v1.0.13/src/system.ts
+// Ported from https://github.com/stackblitz/alien-signals/blob/v2.0.4/src/system.ts
 import type { ComputedRefImpl as Computed } from './computed.js'
 import type { ReactiveEffect as Effect } from './effect.js'
-
-export interface Dependency {
-  subs: Link | undefined
-  subsTail: Link | undefined
-}
-
-export interface Subscriber {
-  flags: SubscriberFlags
-  deps: Link | undefined
-  depsTail: Link | undefined
+import type { EffectScope } from './effectScope.js'
+import { warn } from './warning.js'
+
+export interface ReactiveNode {
+  deps?: Link
+  depsTail?: Link
+  subs?: Link
+  subsTail?: Link
+  flags: ReactiveFlags
 }
 
 export interface Link {
-  dep: Dependency | Computed
-  sub: Subscriber | Computed | Effect
+  dep: ReactiveNode | Computed | Effect | EffectScope
+  sub: ReactiveNode | Computed | Effect | EffectScope
   prevSub: Link | undefined
   nextSub: Link | undefined
+  prevDep: Link | undefined
   nextDep: Link | undefined
 }
 
-export const enum SubscriberFlags {
-  Computed = 1 << 0,
-  Effect = 1 << 1,
-  Tracking = 1 << 2,
-  Recursed = 1 << 4,
-  Dirty = 1 << 5,
-  PendingComputed = 1 << 6,
-  Propagated = Dirty | PendingComputed,
+interface Stack<T> {
+  value: T
+  prev: Stack<T> | undefined
 }
 
-interface OneWayLink<T> {
-  target: T
-  linked: OneWayLink<T> | undefined
+export const enum ReactiveFlags {
+  None = 0,
+  Mutable = 1 << 0,
+  Watching = 1 << 1,
+  RecursedCheck = 1 << 2,
+  Recursed = 1 << 3,
+  Dirty = 1 << 4,
+  Pending = 1 << 5,
 }
 
 const notifyBuffer: (Effect | undefined)[] = []
 
-let batchDepth = 0
+export let batchDepth = 0
+export let activeSub: ReactiveNode | undefined = undefined
+
 let notifyIndex = 0
 let notifyBufferLength = 0
 
+export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined {
+  try {
+    return activeSub
+  } finally {
+    activeSub = sub
+  }
+}
+
 export function startBatch(): void {
   ++batchDepth
 }
 
 export function endBatch(): void {
-  if (!--batchDepth) {
-    processEffectNotifications()
+  if (!--batchDepth && notifyBufferLength) {
+    flush()
   }
 }
 
-export function link(dep: Dependency, sub: Subscriber): Link | undefined {
-  const currentDep = sub.depsTail
-  if (currentDep !== undefined && currentDep.dep === dep) {
+export function link(dep: ReactiveNode, sub: ReactiveNode): void {
+  const prevDep = sub.depsTail
+  if (prevDep !== undefined && prevDep.dep === dep) {
     return
   }
-  const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps
-  if (nextDep !== undefined && nextDep.dep === dep) {
-    sub.depsTail = nextDep
-    return
+  let nextDep: Link | undefined = undefined
+  const recursedCheck = sub.flags & ReactiveFlags.RecursedCheck
+  if (recursedCheck) {
+    nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps
+    if (nextDep !== undefined && nextDep.dep === dep) {
+      sub.depsTail = nextDep
+      return
+    }
   }
-  const depLastSub = dep.subsTail
+  const prevSub = dep.subsTail
   if (
-    depLastSub !== undefined &&
-    depLastSub.sub === sub &&
-    isValidLink(depLastSub, sub)
+    prevSub !== undefined &&
+    prevSub.sub === sub &&
+    (!recursedCheck || isValidLink(prevSub, sub))
   ) {
     return
   }
-  return linkNewDep(dep, sub, nextDep, currentDep)
+  const newLink =
+    (sub.depsTail =
+    dep.subsTail =
+      {
+        dep,
+        sub,
+        prevDep,
+        nextDep,
+        prevSub,
+        nextSub: undefined,
+      })
+  if (nextDep !== undefined) {
+    nextDep.prevDep = newLink
+  }
+  if (prevDep !== undefined) {
+    prevDep.nextDep = newLink
+  } else {
+    sub.deps = newLink
+  }
+  if (prevSub !== undefined) {
+    prevSub.nextSub = newLink
+  } else {
+    dep.subs = newLink
+  }
+}
+
+export function unlink(
+  link: Link,
+  sub: ReactiveNode = link.sub,
+): Link | undefined {
+  const dep = link.dep
+  const prevDep = link.prevDep
+  const nextDep = link.nextDep
+  const nextSub = link.nextSub
+  const prevSub = link.prevSub
+  if (nextDep !== undefined) {
+    nextDep.prevDep = prevDep
+  } else {
+    sub.depsTail = prevDep
+  }
+  if (prevDep !== undefined) {
+    prevDep.nextDep = nextDep
+  } else {
+    sub.deps = nextDep
+  }
+  if (nextSub !== undefined) {
+    nextSub.prevSub = prevSub
+  } else {
+    dep.subsTail = prevSub
+  }
+  if (prevSub !== undefined) {
+    prevSub.nextSub = nextSub
+  } else if ((dep.subs = nextSub) === undefined) {
+    let toRemove = dep.deps
+    if (toRemove !== undefined) {
+      do {
+        toRemove = unlink(toRemove, dep)
+      } while (toRemove !== undefined)
+      dep.flags |= ReactiveFlags.Dirty
+    }
+  }
+  return nextDep
 }
 
-export function propagate(current: Link): void {
-  let next = current.nextSub
-  let branchs: OneWayLink<Link | undefined> | undefined
-  let branchDepth = 0
-  let targetFlag = SubscriberFlags.Dirty
+export function propagate(link: Link): void {
+  let next = link.nextSub
+  let stack: Stack<Link | undefined> | undefined
 
   top: do {
-    const sub = current.sub
-    const subFlags = sub.flags
+    const sub = link.sub
 
-    let shouldNotify = false
+    let flags = sub.flags
 
-    if (
-      !(
-        subFlags &
-        (SubscriberFlags.Tracking |
-          SubscriberFlags.Recursed |
-          SubscriberFlags.Propagated)
-      )
-    ) {
-      sub.flags = subFlags | targetFlag
-      shouldNotify = true
-    } else if (
-      subFlags & SubscriberFlags.Recursed &&
-      !(subFlags & SubscriberFlags.Tracking)
-    ) {
-      sub.flags = (subFlags & ~SubscriberFlags.Recursed) | targetFlag
-      shouldNotify = true
-    } else if (
-      !(subFlags & SubscriberFlags.Propagated) &&
-      isValidLink(current, sub)
-    ) {
-      sub.flags = subFlags | SubscriberFlags.Recursed | targetFlag
-      shouldNotify = (sub as Dependency).subs !== undefined
-    }
-
-    if (shouldNotify) {
-      const subSubs = (sub as Dependency).subs
-      if (subSubs !== undefined) {
-        current = subSubs
-        if (subSubs.nextSub !== undefined) {
-          branchs = { target: next, linked: branchs }
-          ++branchDepth
-          next = current.nextSub
-        }
-        targetFlag = SubscriberFlags.PendingComputed
-        continue
+    if (flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
+      if (
+        !(
+          flags &
+          (ReactiveFlags.RecursedCheck |
+            ReactiveFlags.Recursed |
+            ReactiveFlags.Dirty |
+            ReactiveFlags.Pending)
+        )
+      ) {
+        sub.flags = flags | ReactiveFlags.Pending
+      } else if (
+        !(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))
+      ) {
+        flags = ReactiveFlags.None
+      } else if (!(flags & ReactiveFlags.RecursedCheck)) {
+        sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending
+      } else if (
+        !(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) &&
+        isValidLink(link, sub)
+      ) {
+        sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending
+        flags &= ReactiveFlags.Mutable
+      } else {
+        flags = ReactiveFlags.None
       }
-      if (subFlags & SubscriberFlags.Effect) {
+
+      if (flags & ReactiveFlags.Watching) {
         notifyBuffer[notifyBufferLength++] = sub as Effect
       }
-    } else if (!(subFlags & (SubscriberFlags.Tracking | targetFlag))) {
-      sub.flags = subFlags | targetFlag
-    } else if (
-      !(subFlags & targetFlag) &&
-      subFlags & SubscriberFlags.Propagated &&
-      isValidLink(current, sub)
-    ) {
-      sub.flags = subFlags | targetFlag
+
+      if (flags & ReactiveFlags.Mutable) {
+        const subSubs = sub.subs
+        if (subSubs !== undefined) {
+          link = subSubs
+          if (subSubs.nextSub !== undefined) {
+            stack = { value: next, prev: stack }
+            next = link.nextSub
+          }
+          continue
+        }
+      }
     }
 
-    if ((current = next!) !== undefined) {
-      next = current.nextSub
-      targetFlag = branchDepth
-        ? SubscriberFlags.PendingComputed
-        : SubscriberFlags.Dirty
+    if ((link = next!) !== undefined) {
+      next = link.nextSub
       continue
     }
 
-    while (branchDepth--) {
-      current = branchs!.target!
-      branchs = branchs!.linked
-      if (current !== undefined) {
-        next = current.nextSub
-        targetFlag = branchDepth
-          ? SubscriberFlags.PendingComputed
-          : SubscriberFlags.Dirty
+    while (stack !== undefined) {
+      link = stack.value!
+      stack = stack.prev
+      if (link !== undefined) {
+        next = link.nextSub
         continue top
       }
     }
 
     break
   } while (true)
-
-  if (!batchDepth) {
-    processEffectNotifications()
-  }
 }
 
-export function startTracking(sub: Subscriber): void {
+export function startTracking(sub: ReactiveNode): ReactiveNode | undefined {
   sub.depsTail = undefined
   sub.flags =
-    (sub.flags & ~(SubscriberFlags.Recursed | SubscriberFlags.Propagated)) |
-    SubscriberFlags.Tracking
-}
-
-export function endTracking(sub: Subscriber): void {
-  const depsTail = sub.depsTail
-  if (depsTail !== undefined) {
-    const nextDep = depsTail.nextDep
-    if (nextDep !== undefined) {
-      clearTracking(nextDep)
-      depsTail.nextDep = undefined
-    }
-  } else if (sub.deps !== undefined) {
-    clearTracking(sub.deps)
-    sub.deps = undefined
-  }
-  sub.flags &= ~SubscriberFlags.Tracking
+    (sub.flags &
+      ~(ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending)) |
+    ReactiveFlags.RecursedCheck
+  return setActiveSub(sub)
 }
 
-export function updateDirtyFlag(
-  sub: Subscriber,
-  flags: SubscriberFlags,
-): boolean {
-  if (checkDirty(sub.deps!)) {
-    sub.flags = flags | SubscriberFlags.Dirty
-    return true
-  } else {
-    sub.flags = flags & ~SubscriberFlags.PendingComputed
-    return false
+export function endTracking(
+  sub: ReactiveNode,
+  prevSub: ReactiveNode | undefined,
+): void {
+  if (__DEV__ && activeSub !== sub) {
+    warn(
+      'Active effect was not restored correctly - ' +
+        'this is likely a Vue internal bug.',
+    )
   }
-}
+  activeSub = prevSub
 
-export function processComputedUpdate(
-  computed: Computed,
-  flags: SubscriberFlags,
-): void {
-  if (flags & SubscriberFlags.Dirty || checkDirty(computed.deps!)) {
-    if (computed.update()) {
-      const subs = computed.subs
-      if (subs !== undefined) {
-        shallowPropagate(subs)
-      }
-    }
-  } else {
-    computed.flags = flags & ~SubscriberFlags.PendingComputed
+  const depsTail = sub.depsTail
+  let toRemove = depsTail !== undefined ? depsTail.nextDep : sub.deps
+  while (toRemove !== undefined) {
+    toRemove = unlink(toRemove, sub)
   }
+  sub.flags &= ~ReactiveFlags.RecursedCheck
 }
 
-export function processEffectNotifications(): void {
+export function flush(): void {
   while (notifyIndex < notifyBufferLength) {
     const effect = notifyBuffer[notifyIndex]!
     notifyBuffer[notifyIndex++] = undefined
@@ -224,109 +259,71 @@ export function processEffectNotifications(): void {
   notifyBufferLength = 0
 }
 
-function linkNewDep(
-  dep: Dependency,
-  sub: Subscriber,
-  nextDep: Link | undefined,
-  depsTail: Link | undefined,
-): Link {
-  const newLink: Link = {
-    dep,
-    sub,
-    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
-}
-
-function checkDirty(current: Link): boolean {
-  let prevLinks: OneWayLink<Link> | undefined
+export function checkDirty(link: Link, sub: ReactiveNode): boolean {
+  let stack: Stack<Link> | undefined
   let checkDepth = 0
-  let dirty: boolean
 
   top: do {
-    dirty = false
-    const dep = current.dep
+    const dep = link.dep
+    const depFlags = dep.flags
+
+    let dirty = false
 
-    if (current.sub.flags & SubscriberFlags.Dirty) {
+    if (sub.flags & ReactiveFlags.Dirty) {
       dirty = true
-    } else if ('flags' in dep) {
-      const depFlags = dep.flags
-      if (
-        (depFlags & (SubscriberFlags.Computed | SubscriberFlags.Dirty)) ===
-        (SubscriberFlags.Computed | SubscriberFlags.Dirty)
-      ) {
-        if ((dep as Computed).update()) {
-          const subs = dep.subs!
-          if (subs.nextSub !== undefined) {
-            shallowPropagate(subs)
-          }
-          dirty = true
-        }
-      } else if (
-        (depFlags &
-          (SubscriberFlags.Computed | SubscriberFlags.PendingComputed)) ===
-        (SubscriberFlags.Computed | SubscriberFlags.PendingComputed)
-      ) {
-        if (current.nextSub !== undefined || current.prevSub !== undefined) {
-          prevLinks = { target: current, linked: prevLinks }
+    } else if (
+      (depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) ===
+      (ReactiveFlags.Mutable | ReactiveFlags.Dirty)
+    ) {
+      if ((dep as Computed).update()) {
+        const subs = dep.subs!
+        if (subs.nextSub !== undefined) {
+          shallowPropagate(subs)
         }
-        current = dep.deps!
-        ++checkDepth
-        continue
+        dirty = true
+      }
+    } else if (
+      (depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) ===
+      (ReactiveFlags.Mutable | ReactiveFlags.Pending)
+    ) {
+      if (link.nextSub !== undefined || link.prevSub !== undefined) {
+        stack = { value: link, prev: stack }
       }
+      link = dep.deps!
+      sub = dep
+      ++checkDepth
+      continue
     }
 
-    if (!dirty && current.nextDep !== undefined) {
-      current = current.nextDep
+    if (!dirty && link.nextDep !== undefined) {
+      link = link.nextDep
       continue
     }
 
     while (checkDepth) {
       --checkDepth
-      const sub = current.sub as Computed
       const firstSub = sub.subs!
+      const hasMultipleSubs = firstSub.nextSub !== undefined
+      if (hasMultipleSubs) {
+        link = stack!.value
+        stack = stack!.prev
+      } else {
+        link = firstSub
+      }
       if (dirty) {
-        if (sub.update()) {
-          if (firstSub.nextSub !== undefined) {
-            current = prevLinks!.target
-            prevLinks = prevLinks!.linked
+        if ((sub as Computed).update()) {
+          if (hasMultipleSubs) {
             shallowPropagate(firstSub)
-          } else {
-            current = firstSub
           }
+          sub = link.sub
           continue
         }
       } else {
-        sub.flags &= ~SubscriberFlags.PendingComputed
+        sub.flags &= ~ReactiveFlags.Pending
       }
-      if (firstSub.nextSub !== undefined) {
-        current = prevLinks!.target
-        prevLinks = prevLinks!.linked
-      } else {
-        current = firstSub
-      }
-      if (current.nextDep !== undefined) {
-        current = current.nextDep
+      sub = link.sub
+      if (link.nextDep !== undefined) {
+        link = link.nextDep
         continue top
       }
       dirty = false
@@ -336,21 +333,22 @@ function checkDirty(current: Link): boolean {
   } while (true)
 }
 
-function shallowPropagate(link: Link): void {
+export function shallowPropagate(link: Link): void {
   do {
     const sub = link.sub
+    const nextSub = link.nextSub
     const subFlags = sub.flags
     if (
-      (subFlags & (SubscriberFlags.PendingComputed | SubscriberFlags.Dirty)) ===
-      SubscriberFlags.PendingComputed
+      (subFlags & (ReactiveFlags.Pending | ReactiveFlags.Dirty)) ===
+      ReactiveFlags.Pending
     ) {
-      sub.flags = subFlags | SubscriberFlags.Dirty
+      sub.flags = subFlags | ReactiveFlags.Dirty
     }
-    link = link.nextSub!
+    link = nextSub!
   } while (link !== undefined)
 }
 
-function isValidLink(checkLink: Link, sub: Subscriber): boolean {
+function isValidLink(checkLink: Link, sub: ReactiveNode): boolean {
   const depsTail = sub.depsTail
   if (depsTail !== undefined) {
     let link = sub.deps!
@@ -366,40 +364,3 @@ function isValidLink(checkLink: Link, sub: Subscriber): boolean {
   }
   return false
 }
-
-function clearTracking(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
-    } else {
-      dep.subsTail = prevSub
-    }
-
-    if (prevSub !== undefined) {
-      prevSub.nextSub = nextSub
-    } else {
-      dep.subs = nextSub
-    }
-
-    if (dep.subs === undefined && 'deps' in dep) {
-      const depFlags = dep.flags
-      if (!(depFlags & SubscriberFlags.Dirty)) {
-        dep.flags = depFlags | 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 882916b160000e086a766514784a37d56b441d0f..532169a2dde2386059cae873ef57f85b999a0648 100644 (file)
@@ -8,20 +8,13 @@ import {
   isObject,
   isPlainObject,
   isSet,
-  remove,
 } from '@vue/shared'
 import type { ComputedRef } from './computed'
 import { ReactiveFlags } from './constants'
-import {
-  type DebuggerOptions,
-  type EffectScheduler,
-  ReactiveEffect,
-  pauseTracking,
-  resetTracking,
-} from './effect'
-import { getCurrentScope } from './effectScope'
+import { type DebuggerOptions, ReactiveEffect, cleanup } from './effect'
 import { isReactive, isShallow } from './reactive'
 import { type Ref, isRef } from './ref'
+import { setActiveSub } from './system'
 import { warn } from './warning'
 
 // These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
@@ -49,12 +42,7 @@ export interface WatchOptions<Immediate = boolean> extends DebuggerOptions {
   immediate?: Immediate
   deep?: boolean | number
   once?: boolean
-  scheduler?: WatchScheduler
   onWarn?: (msg: string, ...args: any[]) => void
-  /**
-   * @internal
-   */
-  augmentJob?: (job: (...args: any[]) => void) => void
   /**
    * @internal
    */
@@ -76,10 +64,7 @@ export interface WatchHandle extends WatchStopHandle {
 // initial value for watchers to trigger on undefined initial values
 const INITIAL_WATCHER_VALUE = {}
 
-export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void
-
-const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()
-let activeWatcher: ReactiveEffect | undefined = undefined
+let activeWatcher: WatcherEffect | undefined = undefined
 
 /**
  * Returns the current active effect if there is one.
@@ -102,12 +87,16 @@ export function getCurrentWatcher(): ReactiveEffect<any> | undefined {
 export function onWatcherCleanup(
   cleanupFn: () => void,
   failSilently = false,
-  owner: ReactiveEffect | undefined = activeWatcher,
+  owner: WatcherEffect | undefined = activeWatcher,
 ): void {
   if (owner) {
-    let cleanups = cleanupMap.get(owner)
-    if (!cleanups) cleanupMap.set(owner, (cleanups = []))
-    cleanups.push(cleanupFn)
+    const { call } = owner.options
+    if (call) {
+      owner.cleanups[owner.cleanupsLength++] = () =>
+        call(cleanupFn, WatchErrorCodes.WATCH_CLEANUP)
+    } else {
+      owner.cleanups[owner.cleanupsLength++] = cleanupFn
+    }
   } else if (__DEV__ && !failSilently) {
     warn(
       `onWatcherCleanup() was called when there was no active watcher` +
@@ -116,212 +105,187 @@ export function onWatcherCleanup(
   }
 }
 
-export function watch(
-  source: WatchSource | WatchSource[] | WatchEffect | object,
-  cb?: WatchCallback | null,
-  options: WatchOptions = EMPTY_OBJ,
-): WatchHandle {
-  const { immediate, deep, once, scheduler, augmentJob, call } = options
-
-  const warnInvalidSource = (s: unknown) => {
-    ;(options.onWarn || warn)(
-      `Invalid watch source: `,
-      s,
-      `A watch source can only be a getter/effect function, a ref, ` +
-        `a reactive object, or an array of these types.`,
-    )
-  }
-
-  const reactiveGetter = (source: object) => {
-    // traverse will happen in wrapped getter below
-    if (deep) return source
-    // for `deep: false | 0` or shallow reactive, only traverse root-level properties
-    if (isShallow(source) || deep === false || deep === 0)
-      return traverse(source, 1)
-    // for `deep: undefined` on a reactive object, deeply traverse all properties
-    return traverse(source)
-  }
-
-  let effect: ReactiveEffect
-  let getter: () => any
-  let cleanup: (() => void) | undefined
-  let boundCleanup: typeof onWatcherCleanup
-  let forceTrigger = false
-  let isMultiSource = false
-
-  if (isRef(source)) {
-    getter = () => source.value
-    forceTrigger = isShallow(source)
-  } else if (isReactive(source)) {
-    getter = () => reactiveGetter(source)
-    forceTrigger = true
-  } else if (isArray(source)) {
-    isMultiSource = true
-    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
-    getter = () =>
-      source.map(s => {
-        if (isRef(s)) {
-          return s.value
-        } else if (isReactive(s)) {
-          return reactiveGetter(s)
-        } else if (isFunction(s)) {
-          return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
-        } else {
-          __DEV__ && warnInvalidSource(s)
-        }
-      })
-  } else if (isFunction(source)) {
-    if (cb) {
-      // getter with cb
-      getter = call
-        ? () => call(source, WatchErrorCodes.WATCH_GETTER)
-        : (source as () => any)
-    } else {
-      // no cb -> simple effect
-      getter = () => {
-        if (cleanup) {
-          pauseTracking()
+export class WatcherEffect extends ReactiveEffect {
+  forceTrigger: boolean
+  isMultiSource: boolean
+  oldValue: any
+  boundCleanup: typeof onWatcherCleanup = fn =>
+    onWatcherCleanup(fn, false, this)
+
+  constructor(
+    source: WatchSource | WatchSource[] | WatchEffect | object,
+    public cb?: WatchCallback<any, any> | null | undefined,
+    public options: WatchOptions = EMPTY_OBJ,
+  ) {
+    const { deep, once, call, onWarn } = options
+
+    let getter: () => any
+    let forceTrigger = false
+    let isMultiSource = false
+
+    if (isRef(source)) {
+      getter = () => source.value
+      forceTrigger = isShallow(source)
+    } else if (isReactive(source)) {
+      getter = () => reactiveGetter(source, deep)
+      forceTrigger = true
+    } else if (isArray(source)) {
+      isMultiSource = true
+      forceTrigger = source.some(s => isReactive(s) || isShallow(s))
+      getter = () =>
+        source.map(s => {
+          if (isRef(s)) {
+            return s.value
+          } else if (isReactive(s)) {
+            return reactiveGetter(s, deep)
+          } else if (isFunction(s)) {
+            return call ? call(s, WatchErrorCodes.WATCH_GETTER) : s()
+          } else {
+            __DEV__ && warnInvalidSource(s, onWarn)
+          }
+        })
+    } else if (isFunction(source)) {
+      if (cb) {
+        // getter with cb
+        getter = call
+          ? () => call(source, WatchErrorCodes.WATCH_GETTER)
+          : (source as () => any)
+      } else {
+        // no cb -> simple effect
+        getter = () => {
+          if (this.cleanupsLength) {
+            const prevSub = setActiveSub()
+            try {
+              cleanup(this)
+            } finally {
+              setActiveSub(prevSub)
+            }
+          }
+          const currentEffect = activeWatcher
+          activeWatcher = this
           try {
-            cleanup()
+            return call
+              ? call(source, WatchErrorCodes.WATCH_CALLBACK, [
+                  this.boundCleanup,
+                ])
+              : source(this.boundCleanup)
           } finally {
-            resetTracking()
+            activeWatcher = currentEffect
           }
         }
-        const currentEffect = activeWatcher
-        activeWatcher = effect
-        try {
-          return call
-            ? call(source, WatchErrorCodes.WATCH_CALLBACK, [boundCleanup])
-            : source(boundCleanup)
-        } finally {
-          activeWatcher = currentEffect
-        }
       }
+    } else {
+      getter = NOOP
+      __DEV__ && warnInvalidSource(source, onWarn)
     }
-  } else {
-    getter = NOOP
-    __DEV__ && warnInvalidSource(source)
-  }
 
-  if (cb && deep) {
-    const baseGetter = getter
-    const depth = deep === true ? Infinity : deep
-    getter = () => traverse(baseGetter(), depth)
-  }
+    if (cb && deep) {
+      const baseGetter = getter
+      const depth = deep === true ? Infinity : deep
+      getter = () => traverse(baseGetter(), depth)
+    }
+
+    super(getter)
+    this.forceTrigger = forceTrigger
+    this.isMultiSource = isMultiSource
 
-  const scope = getCurrentScope()
-  const watchHandle: WatchHandle = () => {
-    effect.stop()
-    if (scope && scope.active) {
-      remove(scope.effects, effect)
+    if (once && cb) {
+      const _cb = cb
+      cb = (...args) => {
+        _cb(...args)
+        this.stop()
+      }
     }
-  }
 
-  if (once && cb) {
-    const _cb = cb
-    cb = (...args) => {
-      _cb(...args)
-      watchHandle()
+    this.cb = cb
+
+    this.oldValue = isMultiSource
+      ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
+      : INITIAL_WATCHER_VALUE
+
+    if (__DEV__) {
+      this.onTrack = options.onTrack
+      this.onTrigger = options.onTrigger
     }
   }
 
-  let oldValue: any = isMultiSource
-    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
-    : INITIAL_WATCHER_VALUE
-
-  const job = (immediateFirstRun?: boolean) => {
-    if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
+  run(initialRun = false): void {
+    const oldValue = this.oldValue
+    const newValue = (this.oldValue = super.run())
+    if (!this.cb) {
       return
     }
-    if (cb) {
-      // watch(source, cb)
-      const newValue = effect.run()
-      if (
-        deep ||
-        forceTrigger ||
-        (isMultiSource
-          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
-          : hasChanged(newValue, oldValue))
-      ) {
-        // cleanup before running cb again
-        if (cleanup) {
-          cleanup()
-        }
-        const currentWatcher = activeWatcher
-        activeWatcher = effect
-        try {
-          const args = [
-            newValue,
-            // pass undefined as the old value when it's changed for the first time
-            oldValue === INITIAL_WATCHER_VALUE
-              ? undefined
-              : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
-                ? []
-                : oldValue,
-            boundCleanup,
-          ]
-          oldValue = newValue
-          call
-            ? call(cb!, WatchErrorCodes.WATCH_CALLBACK, args)
-            : // @ts-expect-error
-              cb!(...args)
-        } finally {
-          activeWatcher = currentWatcher
-        }
+    const { immediate, deep, call } = this.options
+    if (initialRun && !immediate) {
+      return
+    }
+    if (
+      deep ||
+      this.forceTrigger ||
+      (this.isMultiSource
+        ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
+        : hasChanged(newValue, oldValue))
+    ) {
+      // cleanup before running cb again
+      cleanup(this)
+      const currentWatcher = activeWatcher
+      activeWatcher = this
+      try {
+        const args = [
+          newValue,
+          // pass undefined as the old value when it's changed for the first time
+          oldValue === INITIAL_WATCHER_VALUE
+            ? undefined
+            : this.isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
+              ? []
+              : oldValue,
+          this.boundCleanup,
+        ]
+        call
+          ? call(this.cb, WatchErrorCodes.WATCH_CALLBACK, args)
+          : // @ts-expect-error
+            this.cb(...args)
+      } finally {
+        activeWatcher = currentWatcher
       }
-    } else {
-      // watchEffect
-      effect.run()
     }
   }
+}
 
-  if (augmentJob) {
-    augmentJob(job)
-  }
-
-  effect = new ReactiveEffect(getter)
-
-  effect.scheduler = scheduler
-    ? () => scheduler(job, false)
-    : (job as EffectScheduler)
-
-  boundCleanup = fn => onWatcherCleanup(fn, false, effect)
+function reactiveGetter(source: object, deep: WatchOptions['deep']): unknown {
+  // traverse will happen in wrapped getter below
+  if (deep) return source
+  // for `deep: false | 0` or shallow reactive, only traverse root-level properties
+  if (isShallow(source) || deep === false || deep === 0)
+    return traverse(source, 1)
+  // for `deep: undefined` on a reactive object, deeply traverse all properties
+  return traverse(source)
+}
 
-  cleanup = effect.onStop = () => {
-    const cleanups = cleanupMap.get(effect)
-    if (cleanups) {
-      if (call) {
-        call(cleanups, WatchErrorCodes.WATCH_CLEANUP)
-      } else {
-        for (const cleanup of cleanups) cleanup()
-      }
-      cleanupMap.delete(effect)
-    }
-  }
+function warnInvalidSource(s: object, onWarn: WatchOptions['onWarn']): void {
+  ;(onWarn || warn)(
+    `Invalid watch source: `,
+    s,
+    `A watch source can only be a getter/effect function, a ref, ` +
+      `a reactive object, or an array of these types.`,
+  )
+}
 
-  if (__DEV__) {
-    effect.onTrack = options.onTrack
-    effect.onTrigger = options.onTrigger
-  }
+export function watch(
+  source: WatchSource | WatchSource[] | WatchEffect | object,
+  cb?: WatchCallback | null,
+  options: WatchOptions = EMPTY_OBJ,
+): WatchHandle {
+  const effect = new WatcherEffect(source, cb, options)
 
-  // initial run
-  if (cb) {
-    if (immediate) {
-      job(true)
-    } else {
-      oldValue = effect.run()
-    }
-  } else if (scheduler) {
-    scheduler(job.bind(null, true), true)
-  } else {
-    effect.run()
-  }
+  effect.run(true)
 
-  watchHandle.pause = effect.pause.bind(effect)
-  watchHandle.resume = effect.resume.bind(effect)
-  watchHandle.stop = watchHandle
+  const stop = effect.stop.bind(effect) as WatchHandle
+  stop.pause = effect.pause.bind(effect)
+  stop.resume = effect.resume.bind(effect)
+  stop.stop = stop
 
-  return watchHandle
+  return stop
 }
 
 export function traverse(
index ff06fbea77439b06b4931a61e75b336f8c271fa1..80a8b4345024ecc97604629723db1b3137c205a7 100644 (file)
@@ -25,7 +25,9 @@ import {
 } from '@vue/runtime-test'
 import {
   type DebuggerEvent,
+  type EffectScope,
   ITERATE_KEY,
+  ReactiveEffect,
   type Ref,
   type ShallowRef,
   TrackOpTypes,
@@ -503,6 +505,52 @@ describe('api: watch', () => {
     expect(cleanupWatch).toHaveBeenCalledTimes(2)
   })
 
+  it('nested calls to baseWatch and onWatcherCleanup', async () => {
+    let calls: string[] = []
+    let source: Ref<number>
+    let copyist: Ref<number>
+    const scope = effectScope()
+
+    scope.run(() => {
+      source = ref(0)
+      copyist = ref(0)
+      // sync flush
+      watchEffect(
+        () => {
+          const current = (copyist.value = source.value)
+          onWatcherCleanup(() => calls.push(`sync ${current}`))
+        },
+        { flush: 'sync' },
+      )
+      // post flush
+      watchEffect(
+        () => {
+          const current = copyist.value
+          onWatcherCleanup(() => calls.push(`post ${current}`))
+        },
+        { flush: 'post' },
+      )
+    })
+
+    await nextTick()
+    expect(calls).toEqual([])
+
+    scope.run(() => source.value++)
+    expect(calls).toEqual(['sync 0'])
+    await nextTick()
+    expect(calls).toEqual(['sync 0', 'post 0'])
+    calls.length = 0
+
+    scope.run(() => source.value++)
+    expect(calls).toEqual(['sync 1'])
+    await nextTick()
+    expect(calls).toEqual(['sync 1', 'post 1'])
+    calls.length = 0
+
+    scope.stop()
+    expect(calls).toEqual(['sync 2', 'post 2'])
+  })
+
   it('flush timing: pre (default)', async () => {
     const count = ref(0)
     const count2 = ref(0)
@@ -1332,16 +1380,15 @@ describe('api: watch', () => {
     render(h(Comp), nodeOps.createElement('div'))
 
     expect(instance!).toBeDefined()
-    expect(instance!.scope.effects).toBeInstanceOf(Array)
     // includes the component's own render effect AND the watcher effect
-    expect(instance!.scope.effects.length).toBe(2)
+    expect(getEffectsCount(instance!.scope)).toBe(2)
 
     _show!.value = false
 
     await nextTick()
     await nextTick()
 
-    expect(instance!.scope.effects.length).toBe(0)
+    expect(getEffectsCount(instance!.scope)).toBe(0)
   })
 
   test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@@ -1489,7 +1536,7 @@ describe('api: watch', () => {
     createApp(Comp).mount(root)
     // should not record watcher in detached scope and only the instance's
     // own update effect
-    expect(instance!.scope.effects.length).toBe(1)
+    expect(getEffectsCount(instance!.scope)).toBe(1)
   })
 
   test('watchEffect should keep running if created in a detached scope', async () => {
@@ -1796,9 +1843,9 @@ describe('api: watch', () => {
     }
     const root = nodeOps.createElement('div')
     createApp(Comp).mount(root)
-    expect(instance!.scope.effects.length).toBe(2)
+    expect(getEffectsCount(instance!.scope)).toBe(2)
     unwatch!()
-    expect(instance!.scope.effects.length).toBe(1)
+    expect(getEffectsCount(instance!.scope)).toBe(1)
 
     const scope = effectScope()
     scope.run(() => {
@@ -1806,14 +1853,14 @@ describe('api: watch', () => {
         console.log(num.value)
       })
     })
-    expect(scope.effects.length).toBe(1)
+    expect(getEffectsCount(scope)).toBe(1)
     unwatch!()
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
 
     scope.run(() => {
       watch(num, () => {}, { once: true, immediate: true })
     })
-    expect(scope.effects.length).toBe(0)
+    expect(getEffectsCount(scope)).toBe(0)
   })
 
   // simplified case of VueUse syncRef
@@ -2011,3 +2058,13 @@ describe('api: watch', () => {
     expect(onCleanup).toBeCalledTimes(0)
   })
 })
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
index f2de08a40328bbae49f2a1c2a029392da19d91a3..90b27eaf4e3014a4c49075d6e98df430df6ac5c0 100644 (file)
@@ -49,8 +49,8 @@ describe('scheduler', () => {
       const job1 = () => {
         calls.push('job1')
 
-        queueJob(job2)
-        queueJob(job3)
+        queueJob(job2, 10)
+        queueJob(job3, 1)
       }
 
       const job2 = () => {
@@ -58,12 +58,10 @@ describe('scheduler', () => {
         queueJob(job4)
         queueJob(job5)
       }
-      job2.id = 10
 
       const job3 = () => {
         calls.push('job3')
       }
-      job3.id = 1
 
       const job4 = () => {
         calls.push('job4')
@@ -125,9 +123,8 @@ describe('scheduler', () => {
         calls.push('cb1')
         queueJob(job1)
       }
-      cb1.flags! |= SchedulerJobFlags.PRE
 
-      queueJob(cb1)
+      queueJob(cb1, undefined, true)
       await nextTick()
       expect(calls).toEqual(['cb1', 'job1'])
     })
@@ -137,30 +134,23 @@ describe('scheduler', () => {
       const job1 = () => {
         calls.push('job1')
       }
-      job1.id = 1
 
       const cb1: SchedulerJob = () => {
         calls.push('cb1')
-        queueJob(job1)
+        queueJob(job1, 1)
         // cb2 should execute before the job
-        queueJob(cb2)
-        queueJob(cb3)
+        queueJob(cb2, 1, true)
+        queueJob(cb3, 1, true)
       }
-      cb1.flags! |= SchedulerJobFlags.PRE
 
       const cb2: SchedulerJob = () => {
         calls.push('cb2')
       }
-      cb2.flags! |= SchedulerJobFlags.PRE
-      cb2.id = 1
-
       const cb3: SchedulerJob = () => {
         calls.push('cb3')
       }
-      cb3.flags! |= SchedulerJobFlags.PRE
-      cb3.id = 1
 
-      queueJob(cb1)
+      queueJob(cb1, undefined, true)
       await nextTick()
       expect(calls).toEqual(['cb1', 'cb2', 'cb3', 'job1'])
     })
@@ -170,41 +160,30 @@ describe('scheduler', () => {
       const job1: SchedulerJob = () => {
         calls.push('job1')
       }
-      job1.id = 1
-      job1.flags! |= SchedulerJobFlags.PRE
       const job2: SchedulerJob = () => {
         calls.push('job2')
-        queueJob(job5)
-        queueJob(job6)
+        queueJob(job5, 2)
+        queueJob(job6, 2, true)
       }
-      job2.id = 2
-      job2.flags! |= SchedulerJobFlags.PRE
       const job3: SchedulerJob = () => {
         calls.push('job3')
       }
-      job3.id = 2
-      job3.flags! |= SchedulerJobFlags.PRE
       const job4: SchedulerJob = () => {
         calls.push('job4')
       }
-      job4.id = 3
-      job4.flags! |= SchedulerJobFlags.PRE
       const job5: SchedulerJob = () => {
         calls.push('job5')
       }
-      job5.id = 2
       const job6: SchedulerJob = () => {
         calls.push('job6')
       }
-      job6.id = 2
-      job6.flags! |= SchedulerJobFlags.PRE
 
       // We need several jobs to test this properly, otherwise
       // findInsertionIndex can yield the correct index by chance
-      queueJob(job4)
-      queueJob(job2)
-      queueJob(job3)
-      queueJob(job1)
+      queueJob(job4, 3, true)
+      queueJob(job2, 2, true)
+      queueJob(job3, 2, true)
+      queueJob(job1, 1, true)
 
       await nextTick()
       expect(calls).toEqual(['job1', 'job2', 'job3', 'job6', 'job5', 'job4'])
@@ -217,8 +196,8 @@ describe('scheduler', () => {
         // when updating the props of a child component. This is handled
         // directly inside `updateComponentPreRender` to avoid non atomic
         // cb triggers (#1763)
-        queueJob(cb1)
-        queueJob(cb2)
+        queueJob(cb1, undefined, true)
+        queueJob(cb2, undefined, true)
         flushPreFlushCbs()
         calls.push('job1')
       }
@@ -227,11 +206,9 @@ describe('scheduler', () => {
         // a cb triggers its parent job, which should be skipped
         queueJob(job1)
       }
-      cb1.flags! |= SchedulerJobFlags.PRE
       const cb2: SchedulerJob = () => {
         calls.push('cb2')
       }
-      cb2.flags! |= SchedulerJobFlags.PRE
 
       queueJob(job1)
       await nextTick()
@@ -242,29 +219,24 @@ describe('scheduler', () => {
       const calls: string[] = []
       const job1: SchedulerJob = () => {
         calls.push('job1')
-        queueJob(job3)
-        queueJob(job4)
+        queueJob(job3, undefined, true)
+        queueJob(job4, undefined, true)
       }
       // job1 has no id
-      job1.flags! |= SchedulerJobFlags.PRE
       const job2: SchedulerJob = () => {
         calls.push('job2')
       }
-      job2.id = 1
-      job2.flags! |= SchedulerJobFlags.PRE
       const job3: SchedulerJob = () => {
         calls.push('job3')
       }
       // job3 has no id
-      job3.flags! |= SchedulerJobFlags.PRE
       const job4: SchedulerJob = () => {
         calls.push('job4')
       }
       // job4 has no id
-      job4.flags! |= SchedulerJobFlags.PRE
 
-      queueJob(job1)
-      queueJob(job2)
+      queueJob(job1, undefined, true)
+      queueJob(job2, 1, true)
       await nextTick()
       expect(calls).toEqual(['job1', 'job3', 'job4', 'job2'])
     })
@@ -273,9 +245,8 @@ describe('scheduler', () => {
     it('queue preFlushCb inside postFlushCb', async () => {
       const spy = vi.fn()
       const cb: SchedulerJob = () => spy()
-      cb.flags! |= SchedulerJobFlags.PRE
       queuePostFlushCb(() => {
-        queueJob(cb)
+        queueJob(cb, undefined, true)
       })
       await nextTick()
       expect(spy).toHaveBeenCalled()
@@ -448,16 +419,13 @@ describe('scheduler', () => {
       const job1: SchedulerJob = () => {
         calls.push('job1')
       }
-      job1.id = 1
-
       const job2: SchedulerJob = () => {
         calls.push('job2')
       }
-      job2.id = 2
 
       queuePostFlushCb(() => {
-        queueJob(job2)
-        queueJob(job1)
+        queueJob(job2, 2)
+        queueJob(job1, 1)
       })
 
       await nextTick()
@@ -471,21 +439,16 @@ describe('scheduler', () => {
     const job1 = () => calls.push('job1')
     // job1 has no id
     const job2 = () => calls.push('job2')
-    job2.id = 2
     const job3 = () => calls.push('job3')
-    job3.id = 1
     const job4: SchedulerJob = () => calls.push('job4')
-    job4.id = 2
-    job4.flags! |= SchedulerJobFlags.PRE
     const job5: SchedulerJob = () => calls.push('job5')
     // job5 has no id
-    job5.flags! |= SchedulerJobFlags.PRE
 
     queueJob(job1)
-    queueJob(job2)
-    queueJob(job3)
-    queueJob(job4)
-    queueJob(job5)
+    queueJob(job2, 2)
+    queueJob(job3, 1)
+    queueJob(job4, 2, true)
+    queueJob(job5, undefined, true)
     await nextTick()
     expect(calls).toEqual(['job5', 'job3', 'job4', 'job2', 'job1'])
   })
@@ -495,13 +458,11 @@ describe('scheduler', () => {
     const cb1 = () => calls.push('cb1')
     // cb1 has no id
     const cb2 = () => calls.push('cb2')
-    cb2.id = 2
     const cb3 = () => calls.push('cb3')
-    cb3.id = 1
 
     queuePostFlushCb(cb1)
-    queuePostFlushCb(cb2)
-    queuePostFlushCb(cb3)
+    queuePostFlushCb(cb2, 2)
+    queuePostFlushCb(cb3, 1)
     await nextTick()
     expect(calls).toEqual(['cb3', 'cb2', 'cb1'])
   })
@@ -550,13 +511,10 @@ describe('scheduler', () => {
         throw err
       }
     })
-    job1.id = 1
-
     const job2: SchedulerJob = vi.fn()
-    job2.id = 2
 
-    queueJob(job1)
-    queueJob(job2)
+    queueJob(job1, 1)
+    queueJob(job2, 2)
 
     try {
       await nextTick()
@@ -570,8 +528,8 @@ describe('scheduler', () => {
     expect(job1).toHaveBeenCalledTimes(1)
     expect(job2).toHaveBeenCalledTimes(0)
 
-    queueJob(job1)
-    queueJob(job2)
+    queueJob(job1, 1)
+    queueJob(job2, 2)
 
     await nextTick()
 
@@ -622,11 +580,10 @@ describe('scheduler', () => {
 
   test('recursive jobs can only be queued once non-recursively', async () => {
     const job: SchedulerJob = vi.fn()
-    job.id = 1
     job.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job)
-    queueJob(job)
+    queueJob(job, 1)
+    queueJob(job, 1)
 
     await nextTick()
 
@@ -638,15 +595,14 @@ describe('scheduler', () => {
 
     const job: SchedulerJob = vi.fn(() => {
       if (recurse) {
-        queueJob(job)
-        queueJob(job)
+        queueJob(job, 1)
+        queueJob(job, 1)
         recurse = false
       }
     })
-    job.id = 1
     job.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job)
+    queueJob(job, 1)
 
     await nextTick()
 
@@ -659,22 +615,19 @@ describe('scheduler', () => {
     const job1: SchedulerJob = () => {
       if (recurse) {
         // job2 is already queued, so this shouldn't do anything
-        queueJob(job2)
+        queueJob(job2, 2)
         recurse = false
       }
     }
-    job1.id = 1
-
     const job2: SchedulerJob = vi.fn(() => {
       if (recurse) {
-        queueJob(job1)
-        queueJob(job2)
+        queueJob(job1, 1)
+        queueJob(job2, 2)
       }
     })
-    job2.id = 2
     job2.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job2)
+    queueJob(job2, 2)
 
     await nextTick()
 
@@ -685,40 +638,35 @@ describe('scheduler', () => {
     let recurse = true
 
     const job1: SchedulerJob = vi.fn(() => {
-      queueJob(job3)
-      queueJob(job3)
+      queueJob(job3, 3, true)
+      queueJob(job3, 3, true)
       flushPreFlushCbs()
     })
-    job1.id = 1
-    job1.flags = SchedulerJobFlags.PRE
 
     const job2: SchedulerJob = vi.fn(() => {
       if (recurse) {
         // job2 does not allow recurse, so this shouldn't do anything
-        queueJob(job2)
+        queueJob(job2, 2, true)
 
         // job3 is already queued, so this shouldn't do anything
-        queueJob(job3)
+        queueJob(job3, 3, true)
         recurse = false
       }
     })
-    job2.id = 2
-    job2.flags = SchedulerJobFlags.PRE
 
     const job3: SchedulerJob = vi.fn(() => {
       if (recurse) {
-        queueJob(job2)
-        queueJob(job3)
+        queueJob(job2, 2, true)
+        queueJob(job3, 3, true)
 
         // The jobs are already queued, so these should have no effect
-        queueJob(job2)
-        queueJob(job3)
+        queueJob(job2, 2, true)
+        queueJob(job3, 3, true)
       }
     })
-    job3.id = 3
-    job3.flags = SchedulerJobFlags.ALLOW_RECURSE | SchedulerJobFlags.PRE
+    job3.flags = SchedulerJobFlags.ALLOW_RECURSE
 
-    queueJob(job1)
+    queueJob(job1, 1, true)
 
     await nextTick()
 
@@ -775,8 +723,7 @@ describe('scheduler', () => {
       spy()
       flushPreFlushCbs()
     }
-    job.flags! |= SchedulerJobFlags.PRE
-    queueJob(job)
+    queueJob(job, undefined, true)
     await nextTick()
     expect(spy).toHaveBeenCalledTimes(1)
   })
@@ -788,18 +735,14 @@ describe('scheduler', () => {
     const job1: SchedulerJob = () => {
       calls.push('job1')
     }
-    job1.id = 1
-    job1.flags! |= SchedulerJobFlags.PRE
 
     const job2: SchedulerJob = () => {
       calls.push('job2')
     }
-    job2.id = 2
-    job2.flags! |= SchedulerJobFlags.PRE
 
     queuePostFlushCb(() => {
-      queueJob(job2)
-      queueJob(job1)
+      queueJob(job2, 2, true)
+      queueJob(job1, 1, true)
 
       // e.g. nested app.mount() call
       flushPreFlushCbs()
@@ -830,14 +773,14 @@ describe('scheduler', () => {
     const cb1 = () => calls.push('cb1')
     // cb1 has no id
     const cb2 = () => calls.push('cb2')
-    cb2.id = -1
     const queueAndFlush = (hook: Function) => {
       queuePostFlushCb(hook)
       flushPostFlushCbs()
     }
 
     queueAndFlush(() => {
-      queuePostFlushCb([cb1, cb2])
+      queuePostFlushCb(cb1)
+      queuePostFlushCb(cb2, -1)
       flushPostFlushCbs()
     })
 
index 93af3a2b01ce7f8f15289168a26f9928a3615bd1..dce4d852def3861c8be7a2c747bb176cff6149cf 100644 (file)
@@ -8,11 +8,7 @@ import type { ComponentPublicInstance } from './componentPublicInstance'
 import { ErrorTypeStrings, callWithAsyncErrorHandling } from './errorHandling'
 import { warn } from './warning'
 import { toHandlerKey } from '@vue/shared'
-import {
-  type DebuggerEvent,
-  pauseTracking,
-  resetTracking,
-} from '@vue/reactivity'
+import { type DebuggerEvent, setActiveSub } from '@vue/reactivity'
 import { LifecycleHooks } from './enums'
 
 export { onActivated, onDeactivated } from './components/KeepAlive'
@@ -33,16 +29,16 @@ export function injectHook(
       (hook.__weh = (...args: unknown[]) => {
         // disable tracking inside all lifecycle hooks
         // since they can potentially be called inside effects.
-        pauseTracking()
+        const prevSub = setActiveSub()
         // Set currentInstance during hook invocation.
         // This assumes the hook does not synchronously trigger other hooks, which
         // can only be false when the user does something really funky.
-        const reset = setCurrentInstance(target)
+        const prev = setCurrentInstance(target)
         try {
           return callWithAsyncErrorHandling(hook, target, type, args)
         } finally {
-          reset()
-          resetTracking()
+          setCurrentInstance(...prev)
+          setActiveSub(prevSub)
         }
       })
     if (prepend) {
index 6a5532ad555d097cb25e15c154789a09a11e91d0..45b1d28f807ccf780c0c274b5ce0591d8aaaec71 100644 (file)
@@ -14,7 +14,6 @@ import {
   createSetupContext,
   getCurrentGenericInstance,
   setCurrentInstance,
-  unsetCurrentInstance,
 } from './component'
 import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
 import type {
@@ -511,7 +510,7 @@ export function withAsyncContext(getAwaitable: () => any): [any, () => void] {
     )
   }
   let awaitable = getAwaitable()
-  unsetCurrentInstance()
+  setCurrentInstance(null, undefined)
   if (isPromise(awaitable)) {
     awaitable = awaitable.catch(e => {
       setCurrentInstance(ctx)
index 8f6168cdf299f03bc44bfcd5b65b40d8a1e04212..7dce012d90b2b6221870df3c608c7a416c0b5bf9 100644 (file)
@@ -1,17 +1,19 @@
 import {
   type WatchOptions as BaseWatchOptions,
   type DebuggerOptions,
+  EffectFlags,
   type ReactiveMarker,
   type WatchCallback,
   type WatchEffect,
   type WatchHandle,
   type WatchSource,
-  watch as baseWatch,
+  WatcherEffect,
 } from '@vue/reactivity'
 import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
 import { EMPTY_OBJ, NOOP, extend, isFunction, isString } from '@vue/shared'
 import {
   type ComponentInternalInstance,
+  type GenericComponentInstance,
   currentInstance,
   isInSSRComponentSetup,
   setCurrentInstance,
@@ -125,7 +127,7 @@ export function watch<
 // implementation
 export function watch<T = any, Immediate extends Readonly<boolean> = false>(
   source: T | WatchSource<T>,
-  cb: any,
+  cb: WatchCallback,
   options?: WatchOptions<Immediate>,
 ): WatchHandle {
   if (__DEV__ && !isFunction(cb)) {
@@ -138,12 +140,57 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
   return doWatch(source as any, cb, options)
 }
 
+class RenderWatcherEffect extends WatcherEffect {
+  job: SchedulerJob
+
+  constructor(
+    instance: GenericComponentInstance | null,
+    source: WatchSource | WatchSource[] | WatchEffect | object,
+    cb: WatchCallback | null,
+    options: BaseWatchOptions,
+    private flush: 'pre' | 'post' | 'sync',
+  ) {
+    super(source, cb, options)
+
+    const job: SchedulerJob = () => {
+      if (this.dirty) {
+        this.run()
+      }
+    }
+    // important: mark the job as a watcher callback so that scheduler knows
+    // it is allowed to self-trigger (#1727)
+    if (cb) {
+      this.flags |= EffectFlags.ALLOW_RECURSE
+      job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
+    }
+    if (instance) {
+      job.i = instance
+    }
+    this.job = job
+  }
+
+  notify(): void {
+    const flags = this.flags
+    if (!(flags & EffectFlags.PAUSED)) {
+      const flush = this.flush
+      const job = this.job
+      if (flush === 'post') {
+        queuePostRenderEffect(job, undefined, job.i ? job.i.suspense : null)
+      } else if (flush === 'pre') {
+        queueJob(job, job.i ? job.i.uid : undefined, true)
+      } else {
+        job()
+      }
+    }
+  }
+}
+
 function doWatch(
   source: WatchSource | WatchSource[] | WatchEffect | object,
   cb: WatchCallback | null,
   options: WatchOptions = EMPTY_OBJ,
 ): WatchHandle {
-  const { immediate, deep, flush, once } = options
+  const { immediate, deep, flush = 'pre', once } = options
 
   if (__DEV__ && !cb) {
     if (immediate !== undefined) {
@@ -190,50 +237,37 @@ function doWatch(
   baseWatchOptions.call = (fn, type, args) =>
     callWithAsyncErrorHandling(fn, instance, type, args)
 
-  // scheduler
-  let isPre = false
-  if (flush === 'post') {
-    baseWatchOptions.scheduler = job => {
-      queuePostRenderEffect(job, instance && instance.suspense)
-    }
-  } else if (flush !== 'sync') {
-    // default: 'pre'
-    isPre = true
-    baseWatchOptions.scheduler = (job, isFirstRun) => {
-      if (isFirstRun) {
-        job()
-      } else {
-        queueJob(job)
-      }
-    }
-  }
+  const effect = new RenderWatcherEffect(
+    instance,
+    source,
+    cb,
+    baseWatchOptions,
+    flush,
+  )
 
-  baseWatchOptions.augmentJob = (job: SchedulerJob) => {
-    // important: mark the job as a watcher callback so that scheduler knows
-    // it is allowed to self-trigger (#1727)
-    if (cb) {
-      job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
-    }
-    if (isPre) {
-      job.flags! |= SchedulerJobFlags.PRE
-      if (instance) {
-        job.id = instance.uid
-        ;(job as SchedulerJob).i = instance
-      }
-    }
+  // initial run
+  if (cb) {
+    effect.run(true)
+  } else if (flush === 'post') {
+    queuePostRenderEffect(effect.job, undefined, instance && instance.suspense)
+  } else {
+    effect.run(true)
   }
 
-  const watchHandle = baseWatch(source, cb, baseWatchOptions)
+  const stop = effect.stop.bind(effect) as WatchHandle
+  stop.pause = effect.pause.bind(effect)
+  stop.resume = effect.resume.bind(effect)
+  stop.stop = stop
 
   if (__SSR__ && isInSSRComponentSetup) {
     if (ssrCleanup) {
-      ssrCleanup.push(watchHandle)
+      ssrCleanup.push(stop)
     } else if (runsImmediately) {
-      watchHandle()
+      stop()
     }
   }
 
-  return watchHandle
+  return stop
 }
 
 // this.$watch
@@ -256,9 +290,9 @@ export function instanceWatch(
     cb = value.handler as Function
     options = value
   }
-  const reset = setCurrentInstance(this)
+  const prev = setCurrentInstance(this)
   const res = doWatch(getter, cb.bind(publicThis), options)
-  reset()
+  setCurrentInstance(...prev)
   return res
 }
 
index b9ae038a49edf7b44f6ba338415c8b4d7b41d7af..1c3568f743d38f7b8b2642a8f47b0abc95e3cd13 100644 (file)
@@ -5,9 +5,8 @@ import {
   TrackOpTypes,
   isRef,
   markRaw,
-  pauseTracking,
   proxyRefs,
-  resetTracking,
+  setActiveSub,
   shallowReadonly,
   track,
 } from '@vue/reactivity'
@@ -97,7 +96,6 @@ import type { RendererElement } from './renderer'
 import {
   setCurrentInstance,
   setInSSRSetupState,
-  unsetCurrentInstance,
 } from './componentCurrentInstance'
 
 export * from './componentCurrentInstance'
@@ -891,10 +889,10 @@ function setupStatefulComponent(
   // 2. call setup()
   const { setup } = Component
   if (setup) {
-    pauseTracking()
+    const prevSub = setActiveSub()
     const setupContext = (instance.setupContext =
       setup.length > 1 ? createSetupContext(instance) : null)
-    const reset = setCurrentInstance(instance)
+    const prev = setCurrentInstance(instance)
     const setupResult = callWithErrorHandling(
       setup,
       instance,
@@ -905,8 +903,8 @@ function setupStatefulComponent(
       ],
     )
     const isAsyncSetup = isPromise(setupResult)
-    resetTracking()
-    reset()
+    setActiveSub(prevSub)
+    setCurrentInstance(...prev)
 
     if ((isAsyncSetup || instance.sp) && !isAsyncWrapper(instance)) {
       // async setup / serverPrefetch, mark as async boundary for useId()
@@ -914,6 +912,9 @@ function setupStatefulComponent(
     }
 
     if (isAsyncSetup) {
+      const unsetCurrentInstance = (): void => {
+        setCurrentInstance(null, undefined)
+      }
       setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
       if (isSSR) {
         // return the promise so server-renderer can wait on it
@@ -1086,13 +1087,13 @@ export function finishComponentSetup(
 
   // support for 2.x options
   if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
-    const reset = setCurrentInstance(instance)
-    pauseTracking()
+    const prevInstance = setCurrentInstance(instance)
+    const prevSub = setActiveSub()
     try {
       applyOptions(instance)
     } finally {
-      resetTracking()
-      reset()
+      setActiveSub(prevSub)
+      setCurrentInstance(...prevInstance)
     }
   }
 
index c091b9c693dbb437d4e2e73a85872c533966566f..7ac52a2e997182fc0a4ffb5d19698e314e2f3a8b 100644 (file)
@@ -4,6 +4,7 @@ import type {
   GenericComponentInstance,
 } from './component'
 import { currentRenderingInstance } from './componentRenderContext'
+import { type EffectScope, setCurrentScope } from '@vue/reactivity'
 
 /**
  * @internal
@@ -25,7 +26,10 @@ export let isInSSRComponentSetup = false
 
 export let setInSSRSetupState: (state: boolean) => void
 
-let internalSetCurrentInstance: (
+/**
+ * @internal
+ */
+export let simpleSetCurrentInstance: (
   instance: GenericComponentInstance | null,
 ) => void
 
@@ -53,7 +57,7 @@ if (__SSR__) {
       else setters[0](v)
     }
   }
-  internalSetCurrentInstance = registerGlobalSetter(
+  simpleSetCurrentInstance = registerGlobalSetter(
     `__VUE_INSTANCE_SETTERS__`,
     v => (currentInstance = v),
   )
@@ -66,7 +70,7 @@ if (__SSR__) {
     v => (isInSSRComponentSetup = v),
   )
 } else {
-  internalSetCurrentInstance = i => {
+  simpleSetCurrentInstance = i => {
     currentInstance = i
   }
   setInSSRSetupState = v => {
@@ -74,34 +78,15 @@ if (__SSR__) {
   }
 }
 
-export const setCurrentInstance = (instance: GenericComponentInstance) => {
-  const prev = currentInstance
-  internalSetCurrentInstance(instance)
-  instance.scope.on()
-  return (): void => {
-    instance.scope.off()
-    internalSetCurrentInstance(prev)
-  }
-}
-
-export const unsetCurrentInstance = (): void => {
-  currentInstance && currentInstance.scope.off()
-  internalSetCurrentInstance(null)
-}
-
-/**
- * Exposed for vapor only. Vapor never runs during SSR so we don't want to pay
- * for the extra overhead
- * @internal
- */
-export const simpleSetCurrentInstance = (
-  i: GenericComponentInstance | null,
-  unset?: GenericComponentInstance | null,
-): void => {
-  currentInstance = i
-  if (unset) {
-    unset.scope.off()
-  } else if (i) {
-    i.scope.on()
+export const setCurrentInstance = (
+  instance: GenericComponentInstance | null,
+  scope: EffectScope | undefined = instance !== null
+    ? instance.scope
+    : undefined,
+): [GenericComponentInstance | null, EffectScope | undefined] => {
+  try {
+    return [currentInstance, setCurrentScope(scope)]
+  } finally {
+    simpleSetCurrentInstance(instance)
   }
 }
index 93be425b333467a6864c1004e662ba47d2d6ec80..a535927cd074695faffbbf2566bcf04377f1a4ee 100644 (file)
@@ -524,7 +524,7 @@ function baseResolveDefault(
   key: string,
 ) {
   let value
-  const reset = setCurrentInstance(instance)
+  const prev = setCurrentInstance(instance)
   const props = toRaw(instance.props)
   value = factory.call(
     __COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
@@ -532,7 +532,7 @@ function baseResolveDefault(
       : null,
     props,
   )
-  reset()
+  setCurrentInstance(...prev)
   return value
 }
 
index d18d5a48b8f76b221039dbd0183edca8c552aa5c..f4244f360e34a6e669d1d50120e07eb1f6a7355e 100644 (file)
@@ -156,16 +156,20 @@ const KeepAliveImpl: ComponentOptions = {
         vnode.slotScopeIds,
         optimized,
       )
-      queuePostRenderEffect(() => {
-        instance.isDeactivated = false
-        if (instance.a) {
-          invokeArrayFns(instance.a)
-        }
-        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
-        if (vnodeHook) {
-          invokeVNodeHook(vnodeHook, instance.parent, vnode)
-        }
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          instance.isDeactivated = false
+          if (instance.a) {
+            invokeArrayFns(instance.a)
+          }
+          const vnodeHook = vnode.props && vnode.props.onVnodeMounted
+          if (vnodeHook) {
+            invokeVNodeHook(vnodeHook, instance.parent, vnode)
+          }
+        },
+        undefined,
+        parentSuspense,
+      )
 
       if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
         // Update components tree
@@ -186,16 +190,20 @@ const KeepAliveImpl: ComponentOptions = {
         keepAliveInstance,
         parentSuspense,
       )
-      queuePostRenderEffect(() => {
-        if (instance.da) {
-          invokeArrayFns(instance.da)
-        }
-        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
-        if (vnodeHook) {
-          invokeVNodeHook(vnodeHook, instance.parent, vnode)
-        }
-        instance.isDeactivated = true
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          if (instance.da) {
+            invokeArrayFns(instance.da)
+          }
+          const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
+          if (vnodeHook) {
+            invokeVNodeHook(vnodeHook, instance.parent, vnode)
+          }
+          instance.isDeactivated = true
+        },
+        undefined,
+        parentSuspense,
+      )
 
       if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
         // Update components tree
@@ -255,12 +263,16 @@ const KeepAliveImpl: ComponentOptions = {
         // if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves
         // avoid caching vnode that not been mounted
         if (isSuspense(keepAliveInstance.subTree.type)) {
-          queuePostRenderEffect(() => {
-            cache.set(
-              pendingCacheKey!,
-              getInnerChild(keepAliveInstance.subTree),
-            )
-          }, keepAliveInstance.subTree.suspense)
+          queuePostRenderEffect(
+            () => {
+              cache.set(
+                pendingCacheKey!,
+                getInnerChild(keepAliveInstance.subTree),
+              )
+            },
+            undefined,
+            keepAliveInstance.subTree.suspense,
+          )
         } else {
           cache.set(pendingCacheKey, getInnerChild(keepAliveInstance.subTree))
         }
@@ -278,7 +290,7 @@ const KeepAliveImpl: ComponentOptions = {
           resetShapeFlag(vnode)
           // but invoke its deactivated hook here
           const da = vnode.component!.da
-          da && queuePostRenderEffect(da, suspense)
+          da && queuePostRenderEffect(da, undefined, suspense)
           return
         }
         unmount(cached)
index 0f6f69c6526d9edd488b5d0586b0b9dd41020585..2539145bd004ae3a2fab976de360e3f4b7e41eb4 100644 (file)
@@ -873,6 +873,7 @@ function normalizeSuspenseSlot(s: any) {
 
 export function queueEffectWithSuspense(
   fn: Function | Function[],
+  id: number | undefined,
   suspense: SuspenseBoundary | null,
 ): void {
   if (suspense && suspense.pendingBranch) {
@@ -882,7 +883,7 @@ export function queueEffectWithSuspense(
       suspense.effects.push(fn)
     }
   } else {
-    queuePostFlushCb(fn)
+    queuePostFlushCb(fn, id)
   }
 }
 
index 21655f9d7511e858c8db81ae9775cdbfa473750a..7e3b132902f09e7663184fabbe46215c35938fa2 100644 (file)
@@ -165,29 +165,37 @@ export const TeleportImpl = {
 
       if (isTeleportDeferred(n2.props)) {
         n2.el!.__isMounted = false
-        queuePostRenderEffect(() => {
-          mountToTarget()
-          delete n2.el!.__isMounted
-        }, parentSuspense)
+        queuePostRenderEffect(
+          () => {
+            mountToTarget()
+            delete n2.el!.__isMounted
+          },
+          undefined,
+          parentSuspense,
+        )
       } else {
         mountToTarget()
       }
     } else {
       if (isTeleportDeferred(n2.props) && n1.el!.__isMounted === false) {
-        queuePostRenderEffect(() => {
-          TeleportImpl.process(
-            n1,
-            n2,
-            container,
-            anchor,
-            parentComponent,
-            parentSuspense,
-            namespace,
-            slotScopeIds,
-            optimized,
-            internals,
-          )
-        }, parentSuspense)
+        queuePostRenderEffect(
+          () => {
+            TeleportImpl.process(
+              n1,
+              n2,
+              container,
+              anchor,
+              parentComponent,
+              parentSuspense,
+              namespace,
+              slotScopeIds,
+              optimized,
+              internals,
+            )
+          },
+          undefined,
+          parentSuspense,
+        )
         return
       }
       // update content
index dfe39bf43870b55b8d7b6ef29c001ea09da1835b..9338e8a92f80abd64c0cec3b04d6226995a5cb48 100644 (file)
@@ -4,8 +4,7 @@ import {
   isReadonly,
   isRef,
   isShallow,
-  pauseTracking,
-  resetTracking,
+  setActiveSub,
   toRaw,
 } from '@vue/reactivity'
 import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
@@ -37,9 +36,9 @@ export function initCustomFormatter(): void {
         return ['div', vueStyle, `VueInstance`]
       } else if (isRef(obj)) {
         // avoid tracking during debugger accessing
-        pauseTracking()
+        const prevSub = setActiveSub()
         const value = obj.value
-        resetTracking()
+        setActiveSub(prevSub)
         return [
           'div',
           {},
index 5897b39df82f8bbc6490f849fe10638df748de9c..5e3902dcb4bea14cd2bce3cb09aa5991b3adc090 100644 (file)
@@ -23,7 +23,7 @@ import { currentRenderingInstance } from './componentRenderContext'
 import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
 import type { ComponentPublicInstance } from './componentPublicInstance'
 import { mapCompatDirectiveHook } from './compat/customDirective'
-import { pauseTracking, resetTracking, traverse } from '@vue/reactivity'
+import { setActiveSub, traverse } from '@vue/reactivity'
 
 export interface DirectiveBinding<
   Value = any,
@@ -187,14 +187,14 @@ export function invokeDirectiveHook(
     if (hook) {
       // disable tracking inside all lifecycle hooks
       // since they can potentially be called inside effects.
-      pauseTracking()
+      const prevSub = setActiveSub()
       callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
         vnode.el,
         binding,
         vnode,
         prevVNode,
       ])
-      resetTracking()
+      setActiveSub(prevSub)
     }
   }
 }
index f8048c5c0e7d32000c5e086479055d7a258d2d6b..0090b6c16ada4a35faed7ea3bf52e5b7c774e965 100644 (file)
@@ -1,4 +1,4 @@
-import { pauseTracking, resetTracking } from '@vue/reactivity'
+import { setActiveSub } from '@vue/reactivity'
 import type { GenericComponentInstance } from './component'
 import { popWarningContext, pushWarningContext, warn } from './warning'
 import { EMPTY_OBJ, isArray, isFunction, isPromise } from '@vue/shared'
@@ -139,13 +139,13 @@ export function handleError(
     }
     // app-level handling
     if (errorHandler) {
-      pauseTracking()
+      const prevSub = setActiveSub()
       callWithErrorHandling(errorHandler, null, ErrorCodes.APP_ERROR_HANDLER, [
         err,
         exposedInstance,
         errorInfo,
       ])
-      resetTracking()
+      setActiveSub(prevSub)
       return
     }
   }
index ed5d8b081a0ff8d9eddc149f21d37938501e9c59..6483e22416f12b6a2b325ee0a2278b181f0451ac 100644 (file)
@@ -100,7 +100,7 @@ function rerender(id: string, newRender?: Function): void {
     } else {
       const i = instance as ComponentInternalInstance
       i.renderCache = []
-      i.update()
+      i.effect.run()
     }
     nextTick(() => {
       isHmrUpdating = false
@@ -160,7 +160,7 @@ function reload(id: string, newComp: HMRComponent): void {
           if (parent.vapor) {
             parent.hmrRerender!()
           } else {
-            ;(parent as ComponentInternalInstance).update()
+            ;(parent as ComponentInternalInstance).effect.run()
           }
           nextTick(() => {
             isHmrUpdating = false
index 640f26eb05cef0707faa16e161e9eeb987e54178..15b3c7512bebb18395ff33ddb5d2c082a6e4d299 100644 (file)
@@ -548,11 +548,15 @@ export function createHydrationFunctions(
         dirs ||
         needCallTransitionHooks
       ) {
-        queueEffectWithSuspense(() => {
-          vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
-          needCallTransitionHooks && transition!.enter(el)
-          dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
-        }, parentSuspense)
+        queueEffectWithSuspense(
+          () => {
+            vnodeHooks && invokeVNodeHook(vnodeHooks, parentComponent, vnode)
+            needCallTransitionHooks && transition!.enter(el)
+            dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
+          },
+          undefined,
+          parentSuspense,
+        )
       }
     }
 
index 1ed6f21df7769a695a0643a6c27c38eaeaf30eef..243bde548c5cff327235a164abcb6aa4cd8e5a79 100644 (file)
@@ -543,6 +543,7 @@ export {
  */
 export {
   currentInstance,
+  setCurrentInstance,
   simpleSetCurrentInstance,
 } from './componentCurrentInstance'
 /**
index 39f652add7c3509abbb93717a322db7b2f7cadf8..bad40f14393292b9443de30996946f86a31192b1 100644 (file)
@@ -57,8 +57,8 @@ import {
 import {
   EffectFlags,
   ReactiveEffect,
-  pauseTracking,
-  resetTracking,
+  setActiveSub,
+  setCurrentScope,
 } from '@vue/reactivity'
 import { updateProps } from './componentProps'
 import { updateSlots } from './componentSlots'
@@ -307,12 +307,16 @@ export enum MoveType {
 
 export const queuePostRenderEffect: (
   fn: SchedulerJobs,
+  id: number | undefined,
   suspense: SuspenseBoundary | null,
 ) => void = __FEATURE_SUSPENSE__
   ? __TEST__
     ? // vitest can't seem to handle eager circular dependency
-      (fn: Function | Function[], suspense: SuspenseBoundary | null) =>
-        queueEffectWithSuspense(fn, suspense)
+      (
+        fn: Function | Function[],
+        id: number | undefined,
+        suspense: SuspenseBoundary | null,
+      ) => queueEffectWithSuspense(fn, id, suspense)
     : queueEffectWithSuspense
   : queuePostFlushCb
 
@@ -744,11 +748,15 @@ function baseCreateRenderer(
       needCallTransitionHooks ||
       dirs
     ) {
-      queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
-        needCallTransitionHooks && transition!.enter(el)
-        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
+          needCallTransitionHooks && transition!.enter(el)
+          dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
+        },
+        undefined,
+        parentSuspense,
+      )
     }
   }
 
@@ -956,10 +964,14 @@ function baseCreateRenderer(
     }
 
     if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
-      queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
-        dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
+          dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
+        },
+        undefined,
+        parentSuspense,
+      )
     }
   }
 
@@ -1307,7 +1319,7 @@ function baseCreateRenderer(
         // normal update
         instance.next = n2
         // instance.update is the reactive effect.
-        instance.update()
+        instance.effect.run()
       }
     } else {
       // no update needed. just copy over properties
@@ -1316,16 +1328,56 @@ function baseCreateRenderer(
     }
   }
 
-  const setupRenderEffect: SetupRenderEffectFn = (
-    instance,
-    initialVNode,
-    container,
-    anchor,
-    parentSuspense,
-    namespace: ElementNamespace,
-    optimized,
-  ) => {
-    const componentUpdateFn = () => {
+  class SetupRenderEffect extends ReactiveEffect {
+    job: SchedulerJob
+
+    constructor(
+      private instance: ComponentInternalInstance,
+      private initialVNode: VNode,
+      private container: RendererElement,
+      private anchor: RendererNode | null,
+      private parentSuspense: SuspenseBoundary | null,
+      private namespace: ElementNamespace,
+      private optimized: boolean,
+    ) {
+      const prevScope = setCurrentScope(instance.scope)
+      super()
+      setCurrentScope(prevScope)
+
+      this.job = instance.job = () => {
+        if (this.dirty) {
+          this.run()
+        }
+      }
+      this.job.i = instance
+
+      if (__DEV__) {
+        this.onTrack = instance.rtc
+          ? e => invokeArrayFns(instance.rtc!, e)
+          : void 0
+        this.onTrigger = instance.rtg
+          ? e => invokeArrayFns(instance.rtg!, e)
+          : void 0
+      }
+    }
+
+    notify(): void {
+      if (!(this.flags & EffectFlags.PAUSED)) {
+        const job = this.job
+        queueJob(job, job.i!.uid)
+      }
+    }
+
+    fn() {
+      const {
+        instance,
+        initialVNode,
+        container,
+        anchor,
+        parentSuspense,
+        namespace,
+        optimized,
+      } = this
       if (!instance.isMounted) {
         let vnodeHook: VNodeHook | null | undefined
         const { el, props } = initialVNode
@@ -1426,7 +1478,7 @@ function baseCreateRenderer(
         }
         // mounted hook
         if (m) {
-          queuePostRenderEffect(m, parentSuspense)
+          queuePostRenderEffect(m, undefined, parentSuspense)
         }
         // onVnodeMounted
         if (
@@ -1436,6 +1488,7 @@ function baseCreateRenderer(
           const scopedInitialVNode = initialVNode
           queuePostRenderEffect(
             () => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
+            undefined,
             parentSuspense,
           )
         }
@@ -1445,6 +1498,7 @@ function baseCreateRenderer(
         ) {
           queuePostRenderEffect(
             () => instance.emit('hook:mounted'),
+            undefined,
             parentSuspense,
           )
         }
@@ -1459,13 +1513,15 @@ function baseCreateRenderer(
             isAsyncWrapper(parent.vnode) &&
             parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
         ) {
-          instance.a && queuePostRenderEffect(instance.a, parentSuspense)
+          instance.a &&
+            queuePostRenderEffect(instance.a, undefined, parentSuspense)
           if (
             __COMPAT__ &&
             isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
           ) {
             queuePostRenderEffect(
               () => instance.emit('hook:activated'),
+              undefined,
               parentSuspense,
             )
           }
@@ -1477,7 +1533,7 @@ function baseCreateRenderer(
         }
 
         // #2458: deference mount-only object parameters to prevent memleaks
-        initialVNode = container = anchor = null as any
+        this.initialVNode = this.container = this.anchor = null as any
       } else {
         let { next, bu, u, parent, vnode } = instance
 
@@ -1495,7 +1551,7 @@ function baseCreateRenderer(
             nonHydratedAsyncRoot.asyncDep!.then(() => {
               // the instance may be destroyed during the time period
               if (!instance.isUnmounted) {
-                componentUpdateFn()
+                this.fn()
               }
             })
             return
@@ -1573,12 +1629,13 @@ function baseCreateRenderer(
         }
         // updated hook
         if (u) {
-          queuePostRenderEffect(u, parentSuspense)
+          queuePostRenderEffect(u, undefined, parentSuspense)
         }
         // onVnodeUpdated
         if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
           queuePostRenderEffect(
             () => invokeVNodeHook(vnodeHook!, parent, next!, vnode),
+            undefined,
             parentSuspense,
           )
         }
@@ -1588,6 +1645,7 @@ function baseCreateRenderer(
         ) {
           queuePostRenderEffect(
             () => instance.emit('hook:updated'),
+            undefined,
             parentSuspense,
           )
         }
@@ -1601,33 +1659,34 @@ function baseCreateRenderer(
         }
       }
     }
+  }
 
+  const setupRenderEffect: SetupRenderEffectFn = (
+    instance,
+    initialVNode,
+    container,
+    anchor,
+    parentSuspense,
+    namespace: ElementNamespace,
+    optimized,
+  ) => {
     // create reactive effect for rendering
-    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.dirty && effect.run())
-    job.i = instance
-    job.id = instance.uid
-    effect.scheduler = () => queueJob(job)
+    const effect = (instance.effect = new SetupRenderEffect(
+      instance,
+      initialVNode,
+      container,
+      anchor,
+      parentSuspense,
+      namespace,
+      optimized,
+    ))
+    instance.update = effect.run.bind(effect)
 
     // allowRecurse
     // #1801, #2043 component render effects should allow recursive updates
     toggleRecurse(instance, true)
 
-    if (__DEV__) {
-      effect.onTrack = instance.rtc
-        ? e => invokeArrayFns(instance.rtc!, e)
-        : void 0
-      effect.onTrigger = instance.rtg
-        ? e => invokeArrayFns(instance.rtg!, e)
-        : void 0
-    }
-
-    update()
+    effect.run()
   }
 
   const updateComponentPreRender = (
@@ -1642,11 +1701,11 @@ function baseCreateRenderer(
     updateProps(instance, nextVNode.props, prevProps, optimized)
     updateSlots(instance, nextVNode.children, optimized)
 
-    pauseTracking()
+    const prevSub = setActiveSub()
     // props update may have triggered pre-flush watchers.
     // flush them before the render update.
     flushPreFlushCbs(instance)
-    resetTracking()
+    setActiveSub(prevSub)
   }
 
   const patchChildren: PatchChildrenFn = (
@@ -2126,7 +2185,11 @@ function baseCreateRenderer(
       if (moveType === MoveType.ENTER) {
         transition!.beforeEnter(el!)
         hostInsert(el!, container, anchor)
-        queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
+        queuePostRenderEffect(
+          () => transition!.enter(el!),
+          undefined,
+          parentSuspense,
+        )
       } else {
         const { leave, delayLeave, afterLeave } = transition!
         const remove = () => {
@@ -2178,9 +2241,9 @@ function baseCreateRenderer(
 
     // unset ref
     if (ref != null) {
-      pauseTracking()
+      const prevSub = setActiveSub()
       setRef(ref, null, parentSuspense, vnode, true)
-      resetTracking()
+      setActiveSub(prevSub)
     }
 
     // #6593 should clean memo cache when unmount
@@ -2273,11 +2336,15 @@ function baseCreateRenderer(
         (vnodeHook = props && props.onVnodeUnmounted)) ||
       shouldInvokeDirs
     ) {
-      queuePostRenderEffect(() => {
-        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
-        shouldInvokeDirs &&
-          invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
-      }, parentSuspense)
+      queuePostRenderEffect(
+        () => {
+          vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
+          shouldInvokeDirs &&
+            invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
+        },
+        undefined,
+        parentSuspense,
+      )
     }
   }
 
@@ -2357,7 +2424,7 @@ function baseCreateRenderer(
     const {
       bum,
       scope,
-      job,
+      effect,
       subTree,
       um,
       m,
@@ -2392,14 +2459,14 @@ function baseCreateRenderer(
 
     // job may be null if a component is unmounted before its async
     // setup has resolved.
-    if (job) {
+    if (effect) {
       // so that scheduler will no longer invoke it
-      job.flags! |= SchedulerJobFlags.DISPOSED
+      effect.stop()
       unmount(subTree, instance, parentSuspense, doRemove)
     }
     // unmounted hook
     if (um) {
-      queuePostRenderEffect(um, parentSuspense)
+      queuePostRenderEffect(um, undefined, parentSuspense)
     }
     if (
       __COMPAT__ &&
@@ -2407,12 +2474,15 @@ function baseCreateRenderer(
     ) {
       queuePostRenderEffect(
         () => instance.emit('hook:destroyed'),
+        undefined,
         parentSuspense,
       )
     }
-    queuePostRenderEffect(() => {
-      instance.isUnmounted = true
-    }, parentSuspense)
+    queuePostRenderEffect(
+      () => (instance.isUnmounted = true),
+      undefined,
+      parentSuspense,
+    )
 
     // A component with async dep inside a pending suspense is unmounted before
     // its async dep resolves. This should remove the dep from the suspense, and
index ca21030dc35d851a7c293e4d1dea84b3c8eacc7d..31fcf8c2d5b2c227cfd39f636efddf1220008841 100644 (file)
@@ -13,7 +13,6 @@ import { isAsyncWrapper } from './apiAsyncComponent'
 import { warn } from './warning'
 import { isRef, toRaw } from '@vue/reactivity'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
-import type { SchedulerJob } from './scheduler'
 import { queuePostRenderEffect } from './renderer'
 import { type ComponentOptions, getComponentPublicInstance } from './component'
 import { knownTemplateRefs } from './helpers/useTemplateRef'
@@ -153,8 +152,7 @@ export function setRef(
         // #1789: for non-null values, set them after render
         // null values means this is unmount and it should not overwrite another
         // ref with the same key
-        ;(doSet as SchedulerJob).id = -1
-        queuePostRenderEffect(doSet, parentSuspense)
+        queuePostRenderEffect(doSet, -1, parentSuspense)
       } else {
         doSet()
       }
index c5b1b23ff7caf2d2c13ef719a69f7c76f8808f77..967e19cd56d0f05347ab93f8f098826ef33afb67 100644 (file)
@@ -1,10 +1,9 @@
-import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
-import { NOOP, isArray } from '@vue/shared'
+import { ErrorCodes, handleError } from './errorHandling'
+import { isArray } from '@vue/shared'
 import { type GenericComponentInstance, getComponentName } from './component'
 
 export enum SchedulerJobFlags {
   QUEUED = 1 << 0,
-  PRE = 1 << 1,
   /**
    * Indicates whether the effect is allowed to recursively trigger itself
    * when managed by the scheduler.
@@ -20,12 +19,12 @@ export enum SchedulerJobFlags {
    * responsibility to perform recursive state mutation that eventually
    * stabilizes (#1727).
    */
-  ALLOW_RECURSE = 1 << 2,
-  DISPOSED = 1 << 3,
+  ALLOW_RECURSE = 1 << 1,
+  DISPOSED = 1 << 2,
 }
 
 export interface SchedulerJob extends Function {
-  id?: number
+  order?: number
   /**
    * flags can technically be undefined, but it can still be used in bitwise
    * operations just like 0.
@@ -40,17 +39,18 @@ export interface SchedulerJob extends Function {
 
 export type SchedulerJobs = SchedulerJob | SchedulerJob[]
 
-const queue: SchedulerJob[] = []
-let flushIndex = -1
+const jobs: SchedulerJob[] = []
 
-const pendingPostFlushCbs: SchedulerJob[] = []
-let activePostFlushCbs: SchedulerJob[] | null = null
+let postJobs: SchedulerJob[] = []
+let activePostJobs: SchedulerJob[] | null = null
+let currentFlushPromise: Promise<void> | null = null
+let jobsLength = 0
+let flushIndex = 0
 let postFlushIndex = 0
 
 const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
-let currentFlushPromise: Promise<void> | null = null
-
 const RECURSION_LIMIT = 100
+
 type CountMap = Map<SchedulerJob, number>
 
 export function nextTick<T = void, R = void>(
@@ -70,48 +70,64 @@ export function nextTick<T = void, R = void>(
 // A pre watcher will have the same id as its component's update job. The
 // watcher should be inserted immediately before the update job. This allows
 // watchers to be skipped if the component is unmounted by the parent update.
-function findInsertionIndex(id: number) {
-  let start = flushIndex + 1
-  let end = queue.length
-
+function findInsertionIndex(
+  order: number,
+  queue: SchedulerJob[],
+  start: number,
+  end: number,
+) {
   while (start < end) {
     const middle = (start + end) >>> 1
-    const middleJob = queue[middle]
-    const middleJobId = getId(middleJob)
-    if (
-      middleJobId < id ||
-      (middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
-    ) {
+    if (queue[middle].order! <= order) {
       start = middle + 1
     } else {
       end = middle
     }
   }
-
   return start
 }
 
 /**
  * @internal for runtime-vapor only
  */
-export function queueJob(job: SchedulerJob): void {
-  if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
-    const jobId = getId(job)
-    const lastJob = queue[queue.length - 1]
+export function queueJob(job: SchedulerJob, id?: number, isPre = false): void {
+  if (
+    queueJobWorker(
+      job,
+      id === undefined ? (isPre ? -2 : Infinity) : isPre ? id * 2 : id * 2 + 1,
+      jobs,
+      jobsLength,
+      flushIndex,
+    )
+  ) {
+    jobsLength++
+    queueFlush()
+  }
+}
+
+function queueJobWorker(
+  job: SchedulerJob,
+  order: number,
+  queue: SchedulerJob[],
+  length: number,
+  flushIndex: number,
+) {
+  const flags = job.flags!
+  if (!(flags & SchedulerJobFlags.QUEUED)) {
+    job.flags! = flags | SchedulerJobFlags.QUEUED
+    job.order = order
     if (
-      !lastJob ||
+      flushIndex === length ||
       // fast path when the job id is larger than the tail
-      (!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
+      order >= queue[length - 1].order!
     ) {
-      queue.push(job)
+      queue[length] = job
     } else {
-      queue.splice(findInsertionIndex(jobId), 0, job)
+      queue.splice(findInsertionIndex(order, queue, flushIndex, length), 0, job)
     }
-
-    job.flags! |= SchedulerJobFlags.QUEUED
-
-    queueFlush()
+    return true
   }
+  return false
 }
 
 const doFlushJobs = () => {
@@ -129,19 +145,23 @@ function queueFlush() {
   }
 }
 
-export function queuePostFlushCb(cb: SchedulerJobs): void {
-  if (!isArray(cb)) {
-    if (activePostFlushCbs && cb.id === -1) {
-      activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
-    } else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
-      pendingPostFlushCbs.push(cb)
-      cb.flags! |= SchedulerJobFlags.QUEUED
+export function queuePostFlushCb(
+  jobs: SchedulerJobs,
+  id: number = Infinity,
+): void {
+  if (!isArray(jobs)) {
+    if (activePostJobs && id === -1) {
+      activePostJobs.splice(postFlushIndex, 0, jobs)
+    } else {
+      queueJobWorker(jobs, id, postJobs, postJobs.length, 0)
     }
   } else {
     // if cb is an array, it is a component lifecycle hook which can only be
     // triggered by a job, which is already deduped in the main queue, so
     // we can skip duplicate check here to improve perf
-    pendingPostFlushCbs.push(...cb)
+    for (const job of jobs) {
+      queueJobWorker(job, id, postJobs, postJobs.length, 0)
+    }
   }
   queueFlush()
 }
@@ -149,58 +169,52 @@ export function queuePostFlushCb(cb: SchedulerJobs): void {
 export function flushPreFlushCbs(
   instance?: GenericComponentInstance,
   seen?: CountMap,
-  // skip the current job
-  i: number = flushIndex + 1,
 ): void {
   if (__DEV__) {
     seen = seen || new Map()
   }
-  for (; i < queue.length; i++) {
-    const cb = queue[i]
-    if (cb && cb.flags! & SchedulerJobFlags.PRE) {
-      if (instance && cb.id !== instance.uid) {
-        continue
-      }
-      if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
-        continue
-      }
-      queue.splice(i, 1)
-      i--
-      if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
-        cb.flags! &= ~SchedulerJobFlags.QUEUED
-      }
-      cb()
-      if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
-        cb.flags! &= ~SchedulerJobFlags.QUEUED
-      }
+  for (let i = flushIndex; i < jobsLength; i++) {
+    const cb = jobs[i]
+    if (cb.order! & 1 || cb.order === Infinity) {
+      continue
+    }
+    if (instance && cb.order !== instance.uid * 2) {
+      continue
+    }
+    if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
+      continue
+    }
+    jobs.splice(i, 1)
+    i--
+    jobsLength--
+    if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
+      cb.flags! &= ~SchedulerJobFlags.QUEUED
+    }
+    cb()
+    if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+      cb.flags! &= ~SchedulerJobFlags.QUEUED
     }
   }
 }
 
 export function flushPostFlushCbs(seen?: CountMap): void {
-  if (pendingPostFlushCbs.length) {
-    const deduped = [...new Set(pendingPostFlushCbs)].sort(
-      (a, b) => getId(a) - getId(b),
-    )
-    pendingPostFlushCbs.length = 0
-
+  if (postJobs.length) {
     // #1947 already has active queue, nested flushPostFlushCbs call
-    if (activePostFlushCbs) {
-      activePostFlushCbs.push(...deduped)
+    if (activePostJobs) {
+      activePostJobs.push(...postJobs)
+      postJobs.length = 0
       return
     }
 
-    activePostFlushCbs = deduped
+    activePostJobs = postJobs
+    postJobs = []
+
     if (__DEV__) {
       seen = seen || new Map()
     }
 
-    for (
-      postFlushIndex = 0;
-      postFlushIndex < activePostFlushCbs.length;
-      postFlushIndex++
-    ) {
-      const cb = activePostFlushCbs[postFlushIndex]
+    while (postFlushIndex < activePostJobs.length) {
+      const cb = activePostJobs[postFlushIndex++]
       if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
         continue
       }
@@ -215,7 +229,8 @@ export function flushPostFlushCbs(seen?: CountMap): void {
         }
       }
     }
-    activePostFlushCbs = null
+
+    activePostJobs = null
     postFlushIndex = 0
   }
 }
@@ -233,60 +248,58 @@ export function flushOnAppMount(): void {
   }
 }
 
-const getId = (job: SchedulerJob): number =>
-  job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
-
 function flushJobs(seen?: CountMap) {
   if (__DEV__) {
-    seen = seen || new Map()
+    seen ||= new Map()
   }
 
-  // conditional usage of checkRecursiveUpdate must be determined out of
-  // try ... catch block since Rollup by default de-optimizes treeshaking
-  // inside try-catch. This can leave all warning code unshaked. Although
-  // they would get eventually shaken by a minifier like terser, some minifiers
-  // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
-  const check = __DEV__
-    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
-    : NOOP
-
   try {
-    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
-      const job = queue[flushIndex]
-      if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
-        if (__DEV__ && check(job)) {
+    while (flushIndex < jobsLength) {
+      const job = jobs[flushIndex]
+      jobs[flushIndex++] = undefined as any
+
+      if (!(job.flags! & SchedulerJobFlags.DISPOSED)) {
+        // conditional usage of checkRecursiveUpdate must be determined out of
+        // try ... catch block since Rollup by default de-optimizes treeshaking
+        // inside try-catch. This can leave all warning code unshaked. Although
+        // they would get eventually shaken by a minifier like terser, some minifiers
+        // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
+        if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
           continue
         }
         if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
           job.flags! &= ~SchedulerJobFlags.QUEUED
         }
-        callWithErrorHandling(
-          job,
-          job.i,
-          job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
-        )
-        if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
-          job.flags! &= ~SchedulerJobFlags.QUEUED
+        try {
+          job()
+        } catch (err) {
+          handleError(
+            err,
+            job.i,
+            job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
+          )
+        } finally {
+          if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
+            job.flags! &= ~SchedulerJobFlags.QUEUED
+          }
         }
       }
     }
   } finally {
     // If there was an error we still need to clear the QUEUED flags
-    for (; flushIndex < queue.length; flushIndex++) {
-      const job = queue[flushIndex]
-      if (job) {
-        job.flags! &= ~SchedulerJobFlags.QUEUED
-      }
+    while (flushIndex < jobsLength) {
+      jobs[flushIndex].flags! &= ~SchedulerJobFlags.QUEUED
+      jobs[flushIndex++] = undefined as any
     }
 
-    flushIndex = -1
-    queue.length = 0
+    flushIndex = 0
+    jobsLength = 0
 
     flushPostFlushCbs(seen)
 
     currentFlushPromise = null
     // If new jobs have been added to either queue, keep flushing
-    if (queue.length || pendingPostFlushCbs.length) {
+    if (jobsLength || postJobs.length) {
       flushJobs(seen)
     }
   }
index 361a2734ba4da81cbd79373d7a89e9cbebd8fef8..ef4057dd3722b919d790034e65b6cdf4f49c1bbe 100644 (file)
@@ -4,7 +4,7 @@ import {
   formatComponentName,
 } from './component'
 import { isFunction, isString } from '@vue/shared'
-import { isRef, pauseTracking, resetTracking, toRaw } from '@vue/reactivity'
+import { isRef, setActiveSub, toRaw } from '@vue/reactivity'
 import { ErrorCodes, callWithErrorHandling } from './errorHandling'
 import { type VNode, isVNode } from './vnode'
 
@@ -41,7 +41,7 @@ export function warn(msg: string, ...args: any[]): void {
 
   // avoid props formatting or warn handler tracking deps that might be mutated
   // during patch, leading to infinite recursion.
-  pauseTracking()
+  const prevSub = setActiveSub()
 
   const entry = stack.length ? stack[stack.length - 1] : null
   const instance = isVNode(entry) ? entry.component : entry
@@ -79,7 +79,7 @@ export function warn(msg: string, ...args: any[]): void {
     console.warn(...warnArgs)
   }
 
-  resetTracking()
+  setActiveSub(prevSub)
   isWarning = false
 }
 
index 068791b8ad27cff01660c41ea5b1b940e83c895d..290c509552ce9e61cc2b251cfe0b1a013bae3f12 100644 (file)
@@ -1,4 +1,6 @@
 import {
+  type EffectScope,
+  ReactiveEffect,
   currentInstance,
   effectScope,
   nextTick,
@@ -298,7 +300,7 @@ describe('apiWatch', () => {
     define(Comp).render()
     // should not record watcher in detached scope
     // the 1 is the props validation effect
-    expect(instance!.scope.effects.length).toBe(1)
+    expect(getEffectsCount(instance!.scope)).toBe(1)
   })
 
   test('watchEffect should keep running if created in a detached scope', async () => {
@@ -336,3 +338,13 @@ describe('apiWatch', () => {
     expect(countW).toBe(2)
   })
 })
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
index 22294b1e7356d45928b435c752984b6498bd2d71..b96a932a2f38913e1abef9b846de5a37dfe36565 100644 (file)
@@ -1,4 +1,6 @@
 import {
+  type EffectScope,
+  ReactiveEffect,
   type Ref,
   inject,
   nextTick,
@@ -305,12 +307,12 @@ describe('component', () => {
 
     const i = instance as VaporComponentInstance
     // watchEffect + renderEffect + props validation effect
-    expect(i.scope.effects.length).toBe(3)
+    expect(getEffectsCount(i.scope)).toBe(3)
     expect(host.innerHTML).toBe('<div>0</div>')
 
     app.unmount()
     expect(host.innerHTML).toBe('')
-    expect(i.scope.effects.length).toBe(0)
+    expect(getEffectsCount(i.scope)).toBe(0)
   })
 
   test('should mount component only with template in production mode', () => {
@@ -353,3 +355,13 @@ describe('component', () => {
     ).toHaveBeenWarned()
   })
 })
+
+function getEffectsCount(scope: EffectScope): number {
+  let n = 0
+  for (let dep = scope.deps; dep !== undefined; dep = dep.nextDep) {
+    if (dep.dep instanceof ReactiveEffect) {
+      n++
+    }
+  }
+  return n
+}
index e879b7103e5d0da1a90774646017185768663c2d..9d07b413541932f0f6c58ddc1a630138b15e866e 100644 (file)
@@ -12,20 +12,13 @@ import {
 } from '../../src/dom/prop'
 import { setStyle } from '../../src/dom/prop'
 import { VaporComponentInstance } from '../../src/component'
-import {
-  currentInstance,
-  ref,
-  simpleSetCurrentInstance,
-} from '@vue/runtime-dom'
+import { ref, setCurrentInstance } from '@vue/runtime-dom'
 
 let removeComponentInstance = NOOP
 beforeEach(() => {
   const instance = new VaporComponentInstance({}, {}, null)
-  const prev = currentInstance
-  simpleSetCurrentInstance(instance)
-  removeComponentInstance = () => {
-    simpleSetCurrentInstance(prev)
-  }
+  const prev = setCurrentInstance(instance)
+  removeComponentInstance = () => setCurrentInstance(...prev)
 })
 afterEach(() => {
   removeComponentInstance()
index 62529149ad4ce09256fe2f3d931f1541b86c336e..9b457f09b940f806128dbe91e056f3198c7d36a1 100644 (file)
@@ -4,8 +4,7 @@ import {
   isReactive,
   isReadonly,
   isShallow,
-  pauseTracking,
-  resetTracking,
+  setActiveSub,
   shallowReadArray,
   shallowRef,
   toReactive,
@@ -104,7 +103,7 @@ export const createFor = (
     const oldLength = oldBlocks.length
     newBlocks = new Array(newLength)
 
-    pauseTracking()
+    const prevSub = setActiveSub()
 
     if (!isMounted) {
       isMounted = true
@@ -293,7 +292,7 @@ export const createFor = (
       frag.nodes.push(parentAnchor)
     }
 
-    resetTracking()
+    setActiveSub(prevSub)
   }
 
   const needKey = renderItem.length > 1
index d3f3cf71819622c7b7ec988d548f5475ee2c754b..7a30d219811b8d559561164d404911ea85544ca4 100644 (file)
@@ -120,8 +120,7 @@ export function setRef(
           warn('Invalid template ref type:', ref, `(${typeof ref})`)
         }
       }
-      doSet.id = -1
-      queuePostFlushCb(doSet)
+      queuePostFlushCb(doSet, -1)
 
       // TODO this gets called repeatedly in renderEffect when it's dynamic ref?
       onScopeDispose(() => {
index b782afd38d35b66c9fed33675d4f4705efbceb3f..e021ce84b051432f5bda391d9459f04453ac9a13 100644 (file)
@@ -6,7 +6,7 @@ import {
   unmountComponent,
 } from './component'
 import { createComment, createTextNode } from './dom/node'
-import { EffectScope, pauseTracking, resetTracking } from '@vue/reactivity'
+import { EffectScope, setActiveSub } from '@vue/reactivity'
 import { isHydrating } from './dom/hydration'
 
 export type Block =
@@ -47,7 +47,7 @@ export class DynamicFragment extends VaporFragment {
     }
     this.current = key
 
-    pauseTracking()
+    const prevSub = setActiveSub()
     const parent = this.anchor.parentNode
 
     // teardown previous branch
@@ -73,7 +73,7 @@ export class DynamicFragment extends VaporFragment {
       parent && insert(this.nodes, parent, this.anchor)
     }
 
-    resetTracking()
+    setActiveSub(prevSub)
   }
 }
 
index ab5f65de7755e3d28714a6a6da011d04d47c759c..da57882c49de648cd4abdda7c133af7cc390b262 100644 (file)
@@ -20,7 +20,7 @@ import {
   pushWarningContext,
   queuePostFlushCb,
   registerHMR,
-  simpleSetCurrentInstance,
+  setCurrentInstance,
   startMeasure,
   unregisterHMR,
   warn,
@@ -30,9 +30,8 @@ import {
   type ShallowRef,
   markRaw,
   onScopeDispose,
-  pauseTracking,
   proxyRefs,
-  resetTracking,
+  setActiveSub,
   unref,
 } from '@vue/reactivity'
 import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
@@ -205,9 +204,8 @@ export function createComponent(
     instance.emitsOptions = normalizeEmitsOptions(component)
   }
 
-  const prev = currentInstance
-  simpleSetCurrentInstance(instance)
-  pauseTracking()
+  const prevInstance = setCurrentInstance(instance)
+  const prevSub = setActiveSub()
 
   if (__DEV__) {
     setupPropsValidation(instance)
@@ -267,8 +265,8 @@ export function createComponent(
     }
   }
 
-  resetTracking()
-  simpleSetCurrentInstance(prev, instance)
+  setActiveSub(prevSub)
+  setCurrentInstance(...prevInstance)
 
   if (__DEV__) {
     popWarningContext()
index 9cf65c57143a4f72b3b466cd5732bde7cd5cead1..7ee99b681708dacb852cafe3030a3d7646c4493b 100644 (file)
@@ -12,12 +12,11 @@ import type { VaporComponent, VaporComponentInstance } from './component'
 import {
   type NormalizedPropsOptions,
   baseNormalizePropsOptions,
-  currentInstance,
   isEmitListener,
   popWarningContext,
   pushWarningContext,
   resolvePropValue,
-  simpleSetCurrentInstance,
+  setCurrentInstance,
   validateProps,
   warn,
 } from '@vue/runtime-dom'
@@ -262,10 +261,9 @@ function resolveDefault(
   factory: (props: Record<string, any>) => unknown,
   instance: VaporComponentInstance,
 ) {
-  const prev = currentInstance
-  simpleSetCurrentInstance(instance)
+  const prev = setCurrentInstance(instance)
   const res = factory.call(null, instance.props)
-  simpleSetCurrentInstance(prev, instance)
+  setCurrentInstance(...prev)
   return res
 }
 
index 741f385861db5efe5eb40f569f8f37596c541971..c96c1afa13054e1230f9cef57a5fa9d9d04e5c19 100644 (file)
@@ -1,8 +1,7 @@
 import {
-  currentInstance,
   popWarningContext,
   pushWarningContext,
-  simpleSetCurrentInstance,
+  setCurrentInstance,
 } from '@vue/runtime-dom'
 import { insert, normalizeBlock, remove } from './block'
 import {
@@ -19,12 +18,11 @@ export function hmrRerender(instance: VaporComponentInstance): void {
   const parent = normalized[0].parentNode!
   const anchor = normalized[normalized.length - 1].nextSibling
   remove(instance.block, parent)
-  const prev = currentInstance
-  simpleSetCurrentInstance(instance)
+  const prev = setCurrentInstance(instance)
   pushWarningContext(instance)
   devRender(instance)
   popWarningContext()
-  simpleSetCurrentInstance(prev, instance)
+  setCurrentInstance(...prev)
   insert(instance.block, parent, anchor)
 }
 
@@ -36,14 +34,13 @@ export function hmrReload(
   const parent = normalized[0].parentNode!
   const anchor = normalized[normalized.length - 1].nextSibling
   unmountComponent(instance, parent)
-  const prev = currentInstance
-  simpleSetCurrentInstance(instance.parent)
+  const prev = setCurrentInstance(instance.parent)
   const newInstance = createComponent(
     newComp,
     instance.rawProps,
     instance.rawSlots,
     instance.isSingleRoot,
   )
-  simpleSetCurrentInstance(prev, instance.parent)
+  setCurrentInstance(...prev)
   mountComponent(newInstance, parent, anchor)
 }
index a9fa9b33562dd9e6c9fad6348951905b772fef15..ac34e8863d2ab8aada626553ef75863480887014 100644 (file)
@@ -1,70 +1,91 @@
-import { ReactiveEffect, getCurrentScope } from '@vue/reactivity'
+import { EffectFlags, type EffectScope, ReactiveEffect } from '@vue/reactivity'
 import {
   type SchedulerJob,
   currentInstance,
   queueJob,
   queuePostFlushCb,
-  simpleSetCurrentInstance,
+  setCurrentInstance,
   startMeasure,
   warn,
 } from '@vue/runtime-dom'
 import { type VaporComponentInstance, isVaporComponent } from './component'
 import { invokeArrayFns } from '@vue/shared'
 
-export function renderEffect(fn: () => void, noLifecycle = false): void {
-  const instance = currentInstance as VaporComponentInstance | null
-  const scope = getCurrentScope()
-  if (__DEV__ && !__TEST__ && !scope && !isVaporComponent(instance)) {
-    warn('renderEffect called without active EffectScope or Vapor instance.')
-  }
+class RenderEffect extends ReactiveEffect {
+  i: VaporComponentInstance | null
+  job: SchedulerJob
+  updateJob: SchedulerJob
+
+  constructor(public render: () => void) {
+    super()
+    const instance = currentInstance as VaporComponentInstance | null
+    if (__DEV__ && !__TEST__ && !this.subs && !isVaporComponent(instance)) {
+      warn('renderEffect called without active EffectScope or Vapor instance.')
+    }
+
+    const job: SchedulerJob = () => {
+      if (this.dirty) {
+        this.run()
+      }
+    }
+    this.updateJob = () => {
+      instance!.isUpdating = false
+      instance!.u && invokeArrayFns(instance!.u)
+    }
 
-  // renderEffect is always called after user has registered all hooks
-  const hasUpdateHooks = instance && (instance.bu || instance.u)
-  const renderEffectFn = noLifecycle
-    ? fn
-    : () => {
-        if (__DEV__ && instance) {
-          startMeasure(instance, `renderEffect`)
-        }
-        const prev = currentInstance
-        simpleSetCurrentInstance(instance)
-        if (scope) scope.on()
-        if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) {
-          instance.isUpdating = true
-          instance.bu && invokeArrayFns(instance.bu)
-          fn()
-          queuePostFlushCb(() => {
-            instance.isUpdating = false
-            instance.u && invokeArrayFns(instance.u)
-          })
-        } else {
-          fn()
-        }
-        if (scope) scope.off()
-        simpleSetCurrentInstance(prev, instance)
-        if (__DEV__ && instance) {
-          startMeasure(instance, `renderEffect`)
-        }
+    if (instance) {
+      if (__DEV__) {
+        this.onTrack = instance.rtc
+          ? e => invokeArrayFns(instance.rtc!, e)
+          : void 0
+        this.onTrigger = instance.rtg
+          ? e => invokeArrayFns(instance.rtg!, e)
+          : void 0
       }
+      job.i = instance
+    }
+
+    this.job = job
+    this.i = instance
 
-  const effect = new ReactiveEffect(renderEffectFn)
-  const job: SchedulerJob = () => effect.dirty && effect.run()
+    // TODO recurse handling
+  }
 
-  if (instance) {
-    if (__DEV__) {
-      effect.onTrack = instance.rtc
-        ? e => invokeArrayFns(instance.rtc!, e)
-        : void 0
-      effect.onTrigger = instance.rtg
-        ? e => invokeArrayFns(instance.rtg!, e)
-        : void 0
+  fn(): void {
+    const instance = this.i
+    const scope = this.subs ? (this.subs.sub as EffectScope) : undefined
+    // renderEffect is always called after user has registered all hooks
+    const hasUpdateHooks = instance && (instance.bu || instance.u)
+    if (__DEV__ && instance) {
+      startMeasure(instance, `renderEffect`)
+    }
+    const prev = setCurrentInstance(instance, scope)
+    if (hasUpdateHooks && instance.isMounted && !instance.isUpdating) {
+      instance.isUpdating = true
+      instance.bu && invokeArrayFns(instance.bu)
+      this.render()
+      queuePostFlushCb(this.updateJob)
+    } else {
+      this.render()
+    }
+    setCurrentInstance(...prev)
+    if (__DEV__ && instance) {
+      startMeasure(instance, `renderEffect`)
     }
-    job.i = instance
-    job.id = instance.uid
   }
 
-  effect.scheduler = () => queueJob(job)
-  effect.run()
+  notify(): void {
+    const flags = this.flags
+    if (!(flags & EffectFlags.PAUSED)) {
+      queueJob(this.job, this.i ? this.i.uid : undefined)
+    }
+  }
+}
 
-  // TODO recurse handling
+export function renderEffect(fn: () => void, noLifecycle = false): void {
+  const effect = new RenderEffect(fn)
+  if (noLifecycle) {
+    effect.fn = fn
+  }
+  effect.run()
 }