toRaw,
triggerRef,
} from '../src'
-import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
import type { ComputedRef, ComputedRefImpl } from '../src/computed'
+import { pauseTracking, resetTracking } from '../src/effect'
+import { SubscriberFlags } from '../src/system'
describe('reactivity/computed', () => {
it('should return updated value', () => {
a.value++
e.value
- expect(e.deps!.dep).toBe(b.dep)
- expect(e.deps!.nextDep!.dep).toBe(d.dep)
- expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
+ expect(e.deps!.dep).toBe(b)
+ expect(e.deps!.nextDep!.dep).toBe(d)
+ expect(e.deps!.nextDep!.nextDep!.dep).toBe(c)
expect(cSpy).toHaveBeenCalledTimes(2)
a.value++
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
c2.value
- expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
- expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
+ expect(c1.flags & SubscriberFlags.Dirtys).toBe(0)
+ expect(c2.flags & SubscriberFlags.Dirtys).toBe(0)
})
it('should chained computeds dirtyLevel update with first computed effect', () => {
+import {
+ computed,
+ h,
+ nextTick,
+ nodeOps,
+ ref,
+ render,
+ serializeInner,
+} from '@vue/runtime-test'
+import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
+import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
import {
type DebuggerEvent,
type ReactiveEffectRunner,
stop,
toRaw,
} from '../src/index'
-import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
-import {
- computed,
- h,
- nextTick,
- nodeOps,
- ref,
- render,
- serializeInner,
-} from '@vue/runtime-test'
-import {
- endBatch,
- onEffectCleanup,
- pauseTracking,
- resetTracking,
- startBatch,
-} from '../src/effect'
+import { type Dependency, endBatch, startBatch } from '../src/system'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
})
describe('dep unsubscribe', () => {
- function getSubCount(dep: Dep | undefined) {
+ function getSubCount(dep: Dependency | undefined) {
let count = 0
let sub = dep!.subs
while (sub) {
count++
- sub = sub.prevSub
+ sub = sub.nextSub
}
return count
}
type ComputedRef,
computed,
effect,
+ effectScope,
reactive,
shallowRef as ref,
toRaw,
}
// #9233
- it('should release computed cache', async () => {
+ it.todo('should release computed cache', async () => {
const src = ref<{} | undefined>({})
// @ts-expect-error ES2021 API
const srcRef = new WeakRef(src.value!)
expect(srcRef.deref()).toBeUndefined()
})
- it('should release reactive property dep', async () => {
+ it.todo('should release reactive property dep', async () => {
const src = reactive({ foo: 1 })
let c: ComputedRef | undefined = computed(() => src.foo)
src.foo++
expect(spy).toHaveBeenCalledTimes(2)
})
+
+ it('should release computed that untrack by effect', async () => {
+ const src = ref(0)
+ // @ts-expect-error ES2021 API
+ const c = new WeakRef(computed(() => src.value))
+ const scope = effectScope()
+
+ scope.run(() => {
+ effect(() => c.deref().value)
+ })
+
+ expect(c.deref()).toBeDefined()
+ scope.stop()
+ await gc()
+ expect(c.deref()).toBeUndefined()
+ })
+
+ it('should release computed that untrack by effectScope', async () => {
+ const src = ref(0)
+ // @ts-expect-error ES2021 API
+ const c = new WeakRef(computed(() => src.value))
+ const scope = effectScope()
+
+ scope.run(() => {
+ c.deref().value
+ })
+
+ expect(c.deref()).toBeDefined()
+ scope.stop()
+ await gc()
+ expect(c.deref()).toBeUndefined()
+ })
})
+import { isArray } from '@vue/shared'
import { TrackOpTypes } from './constants'
-import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
-import { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { ARRAY_ITERATE_KEY, track } from './dep'
-import { isArray } from '@vue/shared'
+import { pauseTracking, resetTracking } from './effect'
+import { isProxy, isShallow, toRaw, toReactive } from './reactive'
+import { endBatch, startBatch } from './system'
/**
* Track array iteration and return:
-import { isFunction } from '@vue/shared'
+import { hasChanged, isFunction } from '@vue/shared'
+import { ReactiveFlags, TrackOpTypes } from './constants'
+import { onTrack, setupFlagsHandler } from './debug'
import {
type DebuggerEvent,
type DebuggerOptions,
- EffectFlags,
- type Subscriber,
activeSub,
- batch,
- refreshComputed,
+ activeTrackId,
+ nextTrackId,
+ setActiveSub,
} from './effect'
+import { activeEffectScope } from './effectScope'
import type { Ref } from './ref'
+import {
+ type Dependency,
+ type IComputed,
+ type Link,
+ SubscriberFlags,
+ checkDirty,
+ endTrack,
+ link,
+ startTrack,
+} from './system'
import { warn } from './warning'
-import { Dep, type Link, globalVersion } from './dep'
-import { ReactiveFlags, TrackOpTypes } from './constants'
declare const ComputedRefSymbol: unique symbol
declare const WritableComputedRefSymbol: unique symbol
* @private exported by @vue/reactivity for Vue core use, but not exported from
* the main vue package
*/
-export class ComputedRefImpl<T = any> implements Subscriber {
+export class ComputedRefImpl<T = any> implements IComputed {
/**
* @internal
*/
- _value: any = undefined
- /**
- * @internal
- */
- readonly dep: Dep = new Dep(this)
+ _value: T | undefined = undefined
+ version = 0
+
+ // Dependency
+ subs: Link | undefined = undefined
+ subsTail: Link | undefined = undefined
+ lastTrackedId = 0
+
+ // Subscriber
+ deps: Link | undefined = undefined
+ depsTail: Link | undefined = undefined
+ flags: SubscriberFlags = SubscriberFlags.Dirty
+
/**
* @internal
*/
*/
readonly __v_isReadonly: boolean
// TODO isolatedDeclarations ReactiveFlags.IS_READONLY
- // A computed is also a subscriber that tracks other deps
- /**
- * @internal
- */
- deps?: Link = undefined
- /**
- * @internal
- */
- depsTail?: Link = undefined
- /**
- * @internal
- */
- flags: EffectFlags = EffectFlags.DIRTY
- /**
- * @internal
- */
- globalVersion: number = globalVersion - 1
- /**
- * @internal
- */
- isSSR: boolean
- /**
- * @internal
- */
- next?: Subscriber = undefined
// for backwards compat
- effect: this = this
+ get effect(): this {
+ return this
+ }
+ // for backwards compat
+ get dep(): Dependency {
+ return this
+ }
+ // for backwards compat
+ get _dirty(): boolean {
+ const flags = this.flags
+ if (flags & SubscriberFlags.Dirty) {
+ return true
+ } else if (flags & SubscriberFlags.ToCheckDirty) {
+ if (checkDirty(this.deps!)) {
+ this.flags |= SubscriberFlags.Dirty
+ return true
+ } else {
+ this.flags &= ~SubscriberFlags.ToCheckDirty
+ return false
+ }
+ }
+ return false
+ }
+ set _dirty(v: boolean) {
+ if (v) {
+ this.flags |= SubscriberFlags.Dirty
+ } else {
+ this.flags &= ~SubscriberFlags.Dirtys
+ }
+ }
+
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined,
- isSSR: boolean,
) {
this[ReactiveFlags.IS_READONLY] = !setter
- this.isSSR = isSSR
- }
-
- /**
- * @internal
- */
- notify(): true | void {
- this.flags |= EffectFlags.DIRTY
- if (
- !(this.flags & EffectFlags.NOTIFIED) &&
- // avoid infinite self recursion
- activeSub !== this
- ) {
- batch(this, true)
- return true
- } else if (__DEV__) {
- // TODO warn
+ if (__DEV__) {
+ setupFlagsHandler(this)
}
}
get value(): T {
- const link = __DEV__
- ? this.dep.track({
+ if (this._dirty) {
+ this.update()
+ }
+ if (activeTrackId !== 0 && this.lastTrackedId !== activeTrackId) {
+ if (__DEV__) {
+ onTrack(activeSub!, {
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
- : this.dep.track()
- refreshComputed(this)
- // sync version after evaluation
- if (link) {
- link.version = this.dep.version
+ }
+ this.lastTrackedId = activeTrackId
+ link(this, activeSub!).version = this.version
+ } else if (
+ activeEffectScope !== undefined &&
+ this.lastTrackedId !== activeEffectScope.trackId
+ ) {
+ link(this, activeEffectScope)
}
- return this._value
+ return this._value!
}
set value(newValue) {
warn('Write operation failed: computed value is readonly')
}
}
+
+ update(): boolean {
+ const prevSub = activeSub
+ const prevTrackId = activeTrackId
+ setActiveSub(this, nextTrackId())
+ startTrack(this)
+ const oldValue = this._value
+ let newValue: T
+ try {
+ newValue = this.fn(oldValue)
+ } finally {
+ setActiveSub(prevSub, prevTrackId)
+ endTrack(this)
+ }
+ if (hasChanged(oldValue, newValue)) {
+ this._value = newValue
+ this.version++
+ return true
+ }
+ return false
+ }
}
/**
setter = getterOrOptions.set
}
- const cRef = new ComputedRefImpl(getter, setter, isSSR)
+ const cRef = new ComputedRefImpl(getter, setter)
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
--- /dev/null
+import { extend } from '@vue/shared'
+import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect'
+import { type Link, type Subscriber, SubscriberFlags } from './system'
+
+export const triggerEventInfos: DebuggerEventExtraInfo[] = []
+
+export function onTrack(
+ sub: Link['sub'],
+ debugInfo: DebuggerEventExtraInfo,
+): void {
+ if (!__DEV__) {
+ throw new Error(
+ `Internal error: onTrack should be called only in development.`,
+ )
+ }
+ if ((sub as ReactiveEffectOptions).onTrack) {
+ ;(sub as ReactiveEffectOptions).onTrack!(
+ extend(
+ {
+ effect: sub,
+ },
+ debugInfo,
+ ),
+ )
+ }
+}
+
+export function onTrigger(sub: Link['sub']): void {
+ if (!__DEV__) {
+ throw new Error(
+ `Internal error: onTrigger should be called only in development.`,
+ )
+ }
+ if ((sub as ReactiveEffectOptions).onTrigger) {
+ const debugInfo = triggerEventInfos[triggerEventInfos.length - 1]
+ ;(sub as ReactiveEffectOptions).onTrigger!(
+ extend(
+ {
+ effect: sub,
+ },
+ debugInfo,
+ ),
+ )
+ }
+}
+
+export function setupFlagsHandler(target: Subscriber): void {
+ if (!__DEV__) {
+ throw new Error(
+ `Internal error: setupFlagsHandler should be called only in development.`,
+ )
+ }
+ // @ts-expect-error
+ target._flags = target.flags
+ Object.defineProperty(target, 'flags', {
+ get() {
+ // @ts-expect-error
+ return target._flags
+ },
+ set(value) {
+ if (
+ // @ts-expect-error
+ !(target._flags >> SubscriberFlags.DirtyFlagsIndex) &&
+ !!(value >> SubscriberFlags.DirtyFlagsIndex)
+ ) {
+ onTrigger(this)
+ }
+ // @ts-expect-error
+ target._flags = value
+ },
+ })
+}
-import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
-import type { ComputedRefImpl } from './computed'
+import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import { type TrackOpTypes, TriggerOpTypes } from './constants'
+import { onTrack, triggerEventInfos } from './debug'
+import { activeSub, activeTrackId } from './effect'
import {
- type DebuggerEventExtraInfo,
- EffectFlags,
- type Subscriber,
- activeSub,
+ type Dependency,
+ type Link,
endBatch,
- shouldTrack,
+ link,
+ propagate,
startBatch,
-} from './effect'
+} from './system'
-/**
- * Incremented every time a reactive change happens
- * This is used to give computed a fast path to avoid re-compute when nothing
- * has changed.
- */
-export let globalVersion = 0
-
-/**
- * Represents a link between a source (Dep) and a subscriber (Effect or Computed).
- * Deps and subs have a many-to-many relationship - each link between a
- * dep and a sub is represented by a Link instance.
- *
- * A Link is also a node in two doubly-linked lists - one for the associated
- * sub to track all its deps, and one for the associated dep to track all its
- * subs.
- *
- * @internal
- */
-export class Link {
- /**
- * - Before each effect run, all previous dep links' version are reset to -1
- * - During the run, a link's version is synced with the source dep on access
- * - After the run, links with version -1 (that were never used) are cleaned
- * up
- */
- version: number
-
- /**
- * Pointers for doubly-linked lists
- */
- nextDep?: Link
- prevDep?: Link
- nextSub?: Link
- prevSub?: Link
- prevActiveLink?: Link
+class Dep implements Dependency {
+ _subs: Link | undefined = undefined
+ subsTail: Link | undefined = undefined
+ lastTrackedId = 0
constructor(
- public sub: Subscriber,
- public dep: Dep,
- ) {
- this.version = dep.version
- this.nextDep =
- this.prevDep =
- this.nextSub =
- this.prevSub =
- this.prevActiveLink =
- undefined
- }
-}
-
-/**
- * @internal
- */
-export class Dep {
- version = 0
- /**
- * Link between this dep and the current active effect
- */
- activeLink?: Link = undefined
-
- /**
- * Doubly linked list representing the subscribing effects (tail)
- */
- subs?: Link = undefined
-
- /**
- * Doubly linked list representing the subscribing effects (head)
- * DEV only, for invoking onTrigger hooks in correct order
- */
- subsHead?: Link
-
- /**
- * For object property deps cleanup
- */
- map?: KeyToDepMap = undefined
- key?: unknown = undefined
+ private map: KeyToDepMap,
+ private key: unknown,
+ ) {}
- /**
- * Subscriber counter
- */
- sc: number = 0
-
- constructor(public computed?: ComputedRefImpl | undefined) {
- if (__DEV__) {
- this.subsHead = undefined
- }
+ get subs(): Link | undefined {
+ return this._subs
}
- track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
- if (!activeSub || !shouldTrack || activeSub === this.computed) {
- return
- }
-
- let link = this.activeLink
- if (link === undefined || link.sub !== activeSub) {
- link = this.activeLink = new Link(activeSub, this)
-
- // add the link to the activeEffect as a dep (as tail)
- if (!activeSub.deps) {
- activeSub.deps = activeSub.depsTail = link
- } else {
- link.prevDep = activeSub.depsTail
- activeSub.depsTail!.nextDep = link
- activeSub.depsTail = link
- }
-
- addSub(link)
- } else if (link.version === -1) {
- // reused from last run - already a sub, just sync version
- link.version = this.version
-
- // If this dep has a next, it means it's not at the tail - move it to the
- // tail. This ensures the effect's dep list is in the order they are
- // accessed during evaluation.
- if (link.nextDep) {
- const next = link.nextDep
- next.prevDep = link.prevDep
- if (link.prevDep) {
- link.prevDep.nextDep = next
- }
-
- link.prevDep = activeSub.depsTail
- link.nextDep = undefined
- activeSub.depsTail!.nextDep = link
- activeSub.depsTail = link
-
- // this was the head - point to the new head
- if (activeSub.deps === link) {
- activeSub.deps = next
- }
- }
- }
-
- if (__DEV__ && activeSub.onTrack) {
- activeSub.onTrack(
- extend(
- {
- effect: activeSub,
- },
- debugInfo,
- ),
- )
- }
-
- return link
- }
-
- trigger(debugInfo?: DebuggerEventExtraInfo): void {
- this.version++
- globalVersion++
- this.notify(debugInfo)
- }
-
- notify(debugInfo?: DebuggerEventExtraInfo): void {
- startBatch()
- try {
- if (__DEV__) {
- // subs are notified and batched in reverse-order and then invoked in
- // original order at the end of the batch, but onTrigger hooks should
- // be invoked in original order here.
- for (let head = this.subsHead; head; head = head.nextSub) {
- if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
- head.sub.onTrigger(
- extend(
- {
- effect: head.sub,
- },
- debugInfo,
- ),
- )
- }
- }
- }
- for (let link = this.subs; link; link = link.prevSub) {
- if (link.sub.notify()) {
- // if notify() returns `true`, this is a computed. Also call notify
- // on its dep - it's called here instead of inside computed's notify
- // in order to reduce call stack depth.
- ;(link.sub as ComputedRefImpl).dep.notify()
- }
- }
- } finally {
- endBatch()
+ set subs(value: Link | undefined) {
+ this._subs = value
+ if (value === undefined) {
+ this.map.delete(this.key)
}
}
}
-function addSub(link: Link) {
- link.dep.sc++
- if (link.sub.flags & EffectFlags.TRACKING) {
- const computed = link.dep.computed
- // computed getting its first subscriber
- // enable tracking + lazily subscribe to all its deps
- if (computed && !link.dep.subs) {
- computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
- for (let l = computed.deps; l; l = l.nextDep) {
- addSub(l)
- }
- }
-
- const currentTail = link.dep.subs
- if (currentTail !== link) {
- link.prevSub = currentTail
- if (currentTail) currentTail.nextSub = link
- }
-
- if (__DEV__ && link.dep.subsHead === undefined) {
- link.dep.subsHead = link
- }
-
- link.dep.subs = link
- }
-}
-
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
* @param key - Identifier of the reactive property to track.
*/
export function track(target: object, type: TrackOpTypes, key: unknown): void {
- if (shouldTrack && activeSub) {
+ if (activeTrackId > 0) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
- depsMap.set(key, (dep = new Dep()))
- dep.map = depsMap
- dep.key = key
+ depsMap.set(key, (dep = new Dep(depsMap, key)))
}
- if (__DEV__) {
- dep.track({
- target,
- type,
- key,
- })
- } else {
- dep.track()
+ if (dep.lastTrackedId !== activeTrackId) {
+ if (__DEV__) {
+ onTrack(activeSub!, {
+ target,
+ type,
+ key,
+ })
+ }
+ dep.lastTrackedId = activeTrackId
+ link(dep, activeSub!)
}
}
}
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
- globalVersion++
return
}
- const run = (dep: Dep | undefined) => {
- if (dep) {
+ const run = (dep: Dependency | undefined) => {
+ if (dep !== undefined && dep.subs !== undefined) {
if (__DEV__) {
- dep.trigger({
+ triggerEventInfos.push({
target,
type,
key,
oldValue,
oldTarget,
})
- } else {
- dep.trigger()
+ }
+ propagate(dep.subs)
+ if (__DEV__) {
+ triggerEventInfos.pop()
}
}
}
export function getDepFromReactive(
object: any,
key: string | number | symbol,
-): Dep | undefined {
+): Dependency | undefined {
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}
-import { extend, hasChanged } from '@vue/shared'
-import type { ComputedRefImpl } from './computed'
+import { extend } from '@vue/shared'
import type { TrackOpTypes, TriggerOpTypes } from './constants'
-import { type Link, globalVersion } from './dep'
+import { setupFlagsHandler } from './debug'
import { activeEffectScope } from './effectScope'
+import {
+ type IEffect,
+ type Link,
+ type Subscriber,
+ SubscriberFlags,
+ checkDirty,
+ endTrack,
+ startTrack,
+} from './system'
import { warn } from './warning'
export type EffectScheduler = (...args: any[]) => any
export interface ReactiveEffectOptions extends DebuggerOptions {
scheduler?: EffectScheduler
- allowRecurse?: boolean
onStop?: () => void
}
effect: ReactiveEffect
}
-export let activeSub: Subscriber | undefined
-
export enum EffectFlags {
/**
* ReactiveEffect only
*/
- ACTIVE = 1 << 0,
- RUNNING = 1 << 1,
- TRACKING = 1 << 2,
- NOTIFIED = 1 << 3,
- DIRTY = 1 << 4,
- ALLOW_RECURSE = 1 << 5,
- PAUSED = 1 << 6,
+ ALLOW_RECURSE = 1 << 2,
+ PAUSED = 1 << 3,
+ NOTIFIED = 1 << 4,
+ STOP = 1 << 5,
}
-/**
- * Subscriber is a type that tracks (or subscribes to) a list of deps.
- */
-export interface Subscriber extends DebuggerOptions {
- /**
- * Head of the doubly linked list representing the deps
- * @internal
- */
- deps?: Link
- /**
- * Tail of the same list
- * @internal
- */
- depsTail?: Link
- /**
- * @internal
- */
- flags: EffectFlags
- /**
- * @internal
- */
- next?: Subscriber
- /**
- * returning `true` indicates it's a computed that needs to call notify
- * on its dep too
- * @internal
- */
- notify(): true | void
-}
+export class ReactiveEffect<T = any> implements IEffect, ReactiveEffectOptions {
+ nextNotify: IEffect | undefined = undefined
-const pausedQueueEffects = new WeakSet<ReactiveEffect>()
+ // Subscriber
+ deps: Link | undefined = undefined
+ depsTail: Link | undefined = undefined
+ flags: number = SubscriberFlags.Dirty
-export class ReactiveEffect<T = any>
- implements Subscriber, ReactiveEffectOptions
-{
- /**
- * @internal
- */
- deps?: Link = undefined
- /**
- * @internal
- */
- depsTail?: Link = undefined
- /**
- * @internal
- */
- flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
- /**
- * @internal
- */
- next?: Subscriber = undefined
/**
* @internal
*/
cleanup?: () => void = undefined
- scheduler?: EffectScheduler = undefined
onStop?: () => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
if (activeEffectScope && activeEffectScope.active) {
activeEffectScope.effects.push(this)
}
+ if (__DEV__) {
+ setupFlagsHandler(this)
+ }
+ }
+
+ get active(): boolean {
+ return !(this.flags & EffectFlags.STOP)
}
pause(): void {
- this.flags |= EffectFlags.PAUSED
+ if (!(this.flags & EffectFlags.PAUSED)) {
+ this.flags |= EffectFlags.PAUSED
+ }
}
resume(): void {
- if (this.flags & EffectFlags.PAUSED) {
+ const flags = this.flags
+ if (flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
- if (pausedQueueEffects.has(this)) {
- pausedQueueEffects.delete(this)
- this.trigger()
- }
+ }
+ if (flags & EffectFlags.NOTIFIED) {
+ this.flags &= ~EffectFlags.NOTIFIED
+ this.notify()
}
}
- /**
- * @internal
- */
notify(): void {
- if (
- this.flags & EffectFlags.RUNNING &&
- !(this.flags & EffectFlags.ALLOW_RECURSE)
- ) {
- return
+ const flags = this.flags
+ if (!(flags & EffectFlags.PAUSED)) {
+ this.scheduler()
+ } else {
+ this.flags |= EffectFlags.NOTIFIED
}
- if (!(this.flags & EffectFlags.NOTIFIED)) {
- batch(this)
+ }
+
+ scheduler(): void {
+ if (this.dirty) {
+ this.run()
}
}
run(): T {
// TODO cleanupEffect
- if (!(this.flags & EffectFlags.ACTIVE)) {
+ if (!this.active) {
// stopped during cleanup
return this.fn()
}
-
- this.flags |= EffectFlags.RUNNING
cleanupEffect(this)
- prepareDeps(this)
- const prevEffect = activeSub
- const prevShouldTrack = shouldTrack
- activeSub = this
- shouldTrack = true
+ const prevSub = activeSub
+ const prevTrackId = activeTrackId
+ setActiveSub(this, nextTrackId())
+ startTrack(this)
try {
return this.fn()
'this is likely a Vue internal bug.',
)
}
- cleanupDeps(this)
- activeSub = prevEffect
- shouldTrack = prevShouldTrack
- this.flags &= ~EffectFlags.RUNNING
+ setActiveSub(prevSub, prevTrackId)
+ endTrack(this)
+ if (
+ this.flags & SubscriberFlags.CanPropagate &&
+ this.flags & EffectFlags.ALLOW_RECURSE
+ ) {
+ this.flags &= ~SubscriberFlags.CanPropagate
+ this.notify()
+ }
}
}
stop(): void {
- if (this.flags & EffectFlags.ACTIVE) {
- for (let link = this.deps; link; link = link.nextDep) {
- removeSub(link)
- }
- this.deps = this.depsTail = undefined
+ if (this.active) {
+ startTrack(this)
+ endTrack(this)
cleanupEffect(this)
this.onStop && this.onStop()
- this.flags &= ~EffectFlags.ACTIVE
- }
- }
-
- trigger(): void {
- if (this.flags & EffectFlags.PAUSED) {
- pausedQueueEffects.add(this)
- } else if (this.scheduler) {
- this.scheduler()
- } else {
- this.runIfDirty()
- }
- }
-
- /**
- * @internal
- */
- runIfDirty(): void {
- if (isDirty(this)) {
- this.run()
+ this.flags |= EffectFlags.STOP
}
}
get dirty(): boolean {
- return isDirty(this)
- }
-}
-
-/**
- * For debugging
- */
-// function printDeps(sub: Subscriber) {
-// let d = sub.deps
-// let ds = []
-// while (d) {
-// ds.push(d)
-// d = d.nextDep
-// }
-// return ds.map(d => ({
-// id: d.id,
-// prev: d.prevDep?.id,
-// next: d.nextDep?.id,
-// }))
-// }
-
-let batchDepth = 0
-let batchedSub: Subscriber | undefined
-let batchedComputed: Subscriber | undefined
-
-export function batch(sub: Subscriber, isComputed = false): void {
- sub.flags |= EffectFlags.NOTIFIED
- if (isComputed) {
- sub.next = batchedComputed
- batchedComputed = sub
- return
- }
- sub.next = batchedSub
- batchedSub = sub
-}
-
-/**
- * @internal
- */
-export function startBatch(): void {
- batchDepth++
-}
-
-/**
- * Run batched effects when all batches have ended
- * @internal
- */
-export function endBatch(): void {
- if (--batchDepth > 0) {
- return
- }
-
- if (batchedComputed) {
- let e: Subscriber | undefined = batchedComputed
- batchedComputed = undefined
- while (e) {
- const next: Subscriber | undefined = e.next
- e.next = undefined
- e.flags &= ~EffectFlags.NOTIFIED
- e = next
- }
- }
-
- let error: unknown
- while (batchedSub) {
- let e: Subscriber | undefined = batchedSub
- batchedSub = undefined
- while (e) {
- const next: Subscriber | undefined = e.next
- e.next = undefined
- e.flags &= ~EffectFlags.NOTIFIED
- if (e.flags & EffectFlags.ACTIVE) {
- try {
- // ACTIVE flag is effect-only
- ;(e as ReactiveEffect).trigger()
- } catch (err) {
- if (!error) error = err
- }
- }
- e = next
- }
- }
-
- if (error) throw error
-}
-
-function prepareDeps(sub: Subscriber) {
- // Prepare deps for tracking, starting from the head
- for (let link = sub.deps; link; link = link.nextDep) {
- // set all previous deps' (if any) version to -1 so that we can track
- // which ones are unused after the run
- link.version = -1
- // store previous active sub if link was being used in another context
- link.prevActiveLink = link.dep.activeLink
- link.dep.activeLink = link
- }
-}
-
-function cleanupDeps(sub: Subscriber) {
- // Cleanup unsued deps
- let head
- let tail = sub.depsTail
- let link = tail
- while (link) {
- const prev = link.prevDep
- if (link.version === -1) {
- if (link === tail) tail = prev
- // unused - remove it from the dep's subscribing effect list
- removeSub(link)
- // also remove it from this effect's dep list
- removeDep(link)
- } else {
- // The new head is the last node seen which wasn't removed
- // from the doubly-linked list
- head = link
- }
-
- // restore previous active link if any
- link.dep.activeLink = link.prevActiveLink
- link.prevActiveLink = undefined
- link = prev
- }
- // set the new head & tail
- sub.deps = head
- sub.depsTail = tail
-}
-
-function isDirty(sub: Subscriber): boolean {
- for (let link = sub.deps; link; link = link.nextDep) {
- if (
- link.dep.version !== link.version ||
- (link.dep.computed &&
- (refreshComputed(link.dep.computed) ||
- link.dep.version !== link.version))
- ) {
+ const flags = this.flags
+ if (flags & SubscriberFlags.Dirty) {
return true
- }
- }
- // @ts-expect-error only for backwards compatibility where libs manually set
- // this flag - e.g. Pinia's testing module
- if (sub._dirty) {
- return true
- }
- return false
-}
-
-/**
- * Returning false indicates the refresh failed
- * @internal
- */
-export function refreshComputed(computed: ComputedRefImpl): undefined {
- if (
- computed.flags & EffectFlags.TRACKING &&
- !(computed.flags & EffectFlags.DIRTY)
- ) {
- return
- }
- computed.flags &= ~EffectFlags.DIRTY
-
- // Global version fast path when no reactive changes has happened since
- // last refresh.
- if (computed.globalVersion === globalVersion) {
- return
- }
- computed.globalVersion = globalVersion
-
- const dep = computed.dep
- computed.flags |= EffectFlags.RUNNING
- // In SSR there will be no render effect, so the computed has no subscriber
- // and therefore tracks no deps, thus we cannot rely on the dirty check.
- // Instead, computed always re-evaluate and relies on the globalVersion
- // fast path above for caching.
- if (
- dep.version > 0 &&
- !computed.isSSR &&
- computed.deps &&
- !isDirty(computed)
- ) {
- computed.flags &= ~EffectFlags.RUNNING
- return
- }
-
- const prevSub = activeSub
- const prevShouldTrack = shouldTrack
- activeSub = computed
- shouldTrack = true
-
- try {
- prepareDeps(computed)
- const value = computed.fn(computed._value)
- if (dep.version === 0 || hasChanged(value, computed._value)) {
- computed._value = value
- dep.version++
- }
- } catch (err) {
- dep.version++
- throw err
- } finally {
- activeSub = prevSub
- shouldTrack = prevShouldTrack
- cleanupDeps(computed)
- computed.flags &= ~EffectFlags.RUNNING
- }
-}
-
-function removeSub(link: Link, soft = false) {
- const { dep, prevSub, nextSub } = link
- if (prevSub) {
- prevSub.nextSub = nextSub
- link.prevSub = undefined
- }
- if (nextSub) {
- nextSub.prevSub = prevSub
- link.nextSub = undefined
- }
- if (__DEV__ && dep.subsHead === link) {
- // was previous head, point new head to next
- dep.subsHead = nextSub
- }
-
- if (dep.subs === link) {
- // was previous tail, point new tail to prev
- dep.subs = prevSub
-
- if (!prevSub && dep.computed) {
- // if computed, unsubscribe it from all its deps so this computed and its
- // value can be GCed
- dep.computed.flags &= ~EffectFlags.TRACKING
- for (let l = dep.computed.deps; l; l = l.nextDep) {
- // here we are only "soft" unsubscribing because the computed still keeps
- // referencing the deps and the dep should not decrease its sub count
- removeSub(l, true)
+ } else if (flags & SubscriberFlags.ToCheckDirty) {
+ if (checkDirty(this.deps!)) {
+ this.flags |= SubscriberFlags.Dirty
+ return true
+ } else {
+ this.flags &= ~SubscriberFlags.ToCheckDirty
+ return false
}
}
- }
-
- if (!soft && !--dep.sc && dep.map) {
- // #11979
- // property dep no longer has effect subscribers, delete it
- // this mostly is for the case where an object is kept in memory but only a
- // subset of its properties is tracked at one time
- dep.map.delete(dep.key)
- }
-}
-
-function removeDep(link: Link) {
- const { prevDep, nextDep } = link
- if (prevDep) {
- prevDep.nextDep = nextDep
- link.prevDep = undefined
- }
- if (nextDep) {
- nextDep.prevDep = prevDep
- link.nextDep = undefined
+ return false
}
}
runner.effect.stop()
}
-/**
- * @internal
- */
-export let shouldTrack = true
-const trackStack: boolean[] = []
+const resetTrackingStack: [sub: typeof activeSub, trackId: number][] = []
/**
* Temporarily pauses tracking.
*/
export function pauseTracking(): void {
- trackStack.push(shouldTrack)
- shouldTrack = false
+ resetTrackingStack.push([activeSub, activeTrackId])
+ activeSub = undefined
+ activeTrackId = 0
}
/**
* Re-enables effect tracking (if it was paused).
*/
export function enableTracking(): void {
- trackStack.push(shouldTrack)
- shouldTrack = true
+ const isPaused = activeSub === undefined
+ if (!isPaused) {
+ // Add the current active effect to the trackResetStack so it can be
+ // restored by calling resetTracking.
+ resetTrackingStack.push([activeSub, activeTrackId])
+ } else {
+ // Add a placeholder to the trackResetStack so we can it can be popped
+ // to restore the previous active effect.
+ resetTrackingStack.push([undefined, 0])
+ for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
+ if (resetTrackingStack[i][0] !== undefined) {
+ ;[activeSub, activeTrackId] = resetTrackingStack[i]
+ break
+ }
+ }
+ }
}
/**
* Resets the previous global effect tracking state.
*/
export function resetTracking(): void {
- const last = trackStack.pop()
- shouldTrack = last === undefined ? true : last
+ if (__DEV__ && resetTrackingStack.length === 0) {
+ warn(
+ `resetTracking() was called when there was no active tracking ` +
+ `to reset.`,
+ )
+ }
+ if (resetTrackingStack.length) {
+ ;[activeSub, activeTrackId] = resetTrackingStack.pop()!
+ } else {
+ activeSub = undefined
+ activeTrackId = 0
+ }
}
/**
function cleanupEffect(e: ReactiveEffect) {
const { cleanup } = e
e.cleanup = undefined
- if (cleanup) {
+ if (cleanup !== undefined) {
// run cleanup without active effect
const prevSub = activeSub
activeSub = undefined
}
}
}
+
+export let activeSub: Subscriber | undefined = undefined
+export let activeTrackId = 0
+export let lastTrackId = 0
+export const nextTrackId = (): number => ++lastTrackId
+
+export function setActiveSub(
+ sub: Subscriber | undefined,
+ trackId: number,
+): void {
+ activeSub = sub
+ activeTrackId = trackId
+}
-import type { ReactiveEffect } from './effect'
+import { EffectFlags, type ReactiveEffect, nextTrackId } from './effect'
+import {
+ type Link,
+ type Subscriber,
+ SubscriberFlags,
+ endTrack,
+ startTrack,
+} from './system'
import { warn } from './warning'
export let activeEffectScope: EffectScope | undefined
-export class EffectScope {
- /**
- * @internal
- */
- private _active = true
+export class EffectScope implements Subscriber {
+ // Subscriber: In order to collect orphans computeds
+ deps: Link | undefined = undefined
+ depsTail: Link | undefined = undefined
+ flags: number = SubscriberFlags.None
+
+ trackId: number = nextTrackId()
+
/**
* @internal
*/
*/
cleanups: (() => void)[] = []
- private _isPaused = false
-
/**
* only assigned by undetached scope
* @internal
}
get active(): boolean {
- return this._active
+ return !(this.flags & EffectFlags.STOP)
}
pause(): void {
- if (this._active) {
- this._isPaused = true
+ if (!(this.flags & EffectFlags.PAUSED)) {
+ this.flags |= EffectFlags.PAUSED
let i, l
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
* Resumes the effect scope, including all child scopes and effects.
*/
resume(): void {
- if (this._active) {
- if (this._isPaused) {
- this._isPaused = false
- let i, l
- if (this.scopes) {
- for (i = 0, l = this.scopes.length; i < l; i++) {
- this.scopes[i].resume()
- }
- }
- for (i = 0, l = this.effects.length; i < l; i++) {
- this.effects[i].resume()
+ if (this.flags & EffectFlags.PAUSED) {
+ this.flags &= ~EffectFlags.PAUSED
+ let i, l
+ if (this.scopes) {
+ for (i = 0, l = this.scopes.length; i < l; i++) {
+ this.scopes[i].resume()
}
}
+ for (i = 0, l = this.effects.length; i < l; i++) {
+ this.effects[i].resume()
+ }
}
}
run<T>(fn: () => T): T | undefined {
- if (this._active) {
+ if (this.active) {
const currentEffectScope = activeEffectScope
try {
activeEffectScope = this
}
stop(fromParent?: boolean): void {
- if (this._active) {
- this._active = false
+ if (this.active) {
+ this.flags |= EffectFlags.STOP
+ startTrack(this)
+ endTrack(this)
let i, l
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
isFunction,
isObject,
} from '@vue/shared'
-import { Dep, getDepFromReactive } from './dep'
+import type { ComputedRef, WritableComputedRef } from './computed'
+import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
+import { onTrack, triggerEventInfos } from './debug'
+import { getDepFromReactive } from './dep'
+import { activeSub, activeTrackId } from './effect'
import {
type Builtin,
type ShallowReactiveMarker,
toRaw,
toReactive,
} from './reactive'
-import type { ComputedRef, WritableComputedRef } from './computed'
-import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
+import { type Dependency, type Link, link, propagate } from './system'
import { warn } from './warning'
declare const RefSymbol: unique symbol
/**
* @internal
*/
-class RefImpl<T = any> {
+class RefImpl<T = any> implements Dependency {
+ // Dependency
+ subs: Link | undefined = undefined
+ subsTail: Link | undefined = undefined
+ lastTrackedId = 0
+
_value: T
private _rawValue: T
- dep: Dep = new Dep()
-
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
+ get dep() {
+ return this
+ }
+
get value() {
- if (__DEV__) {
- this.dep.track({
- target: this,
- type: TrackOpTypes.GET,
- key: 'value',
- })
- } else {
- this.dep.track()
- }
+ trackRef(this)
return this._value
}
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
if (__DEV__) {
- this.dep.trigger({
+ triggerEventInfos.push({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
- } else {
- this.dep.trigger()
+ }
+ triggerRef(this as unknown as Ref)
+ if (__DEV__) {
+ triggerEventInfos.pop()
}
}
}
*/
export function triggerRef(ref: Ref): void {
// ref may be an instance of ObjectRefImpl
- if ((ref as unknown as RefImpl).dep) {
+ const dep = (ref as unknown as RefImpl).dep
+ if (dep !== undefined && dep.subs !== undefined) {
+ propagate(dep.subs)
+ }
+}
+
+function trackRef(dep: Dependency) {
+ if (activeTrackId !== 0 && dep.lastTrackedId !== activeTrackId) {
if (__DEV__) {
- ;(ref as unknown as RefImpl).dep.trigger({
- target: ref,
- type: TriggerOpTypes.SET,
+ onTrack(activeSub!, {
+ target: dep,
+ type: TrackOpTypes.GET,
key: 'value',
- newValue: (ref as unknown as RefImpl)._value,
})
- } else {
- ;(ref as unknown as RefImpl).dep.trigger()
}
+ dep.lastTrackedId = activeTrackId
+ link(dep, activeSub!)
}
}
set: (value: T) => void
}
-class CustomRefImpl<T> {
- public dep: Dep
+class CustomRefImpl<T> implements Dependency {
+ // Dependency
+ subs: Link | undefined = undefined
+ subsTail: Link | undefined = undefined
+ lastTrackedId = 0
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public _value: T = undefined!
constructor(factory: CustomRefFactory<T>) {
- const dep = (this.dep = new Dep())
- const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
+ const { get, set } = factory(
+ () => trackRef(this),
+ () => triggerRef(this as unknown as Ref),
+ )
this._get = get
this._set = set
}
+ get dep() {
+ return this
+ }
+
get value() {
return (this._value = this._get())
}
this._object[this._key] = newVal
}
- get dep(): Dep | undefined {
+ get dep(): Dependency | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}
--- /dev/null
+// Ported from https://github.com/stackblitz/alien-signals/blob/v0.4.4/src/system.ts
+
+export interface IEffect extends Subscriber {
+ nextNotify: IEffect | undefined
+ notify(): void
+}
+
+export interface IComputed extends Dependency, Subscriber {
+ version: number
+ update(): boolean
+}
+
+export interface Dependency {
+ subs: Link | undefined
+ subsTail: Link | undefined
+ lastTrackedId?: number
+}
+
+export interface Subscriber {
+ flags: SubscriberFlags
+ deps: Link | undefined
+ depsTail: Link | undefined
+}
+
+export interface Link {
+ dep: Dependency | IComputed | (Dependency & IEffect)
+ sub: Subscriber | IComputed | (Dependency & IEffect) | IEffect
+ version: number
+ // Reuse to link prev stack in checkDirty
+ // Reuse to link prev stack in propagate
+ prevSub: Link | undefined
+ nextSub: Link | undefined
+ // Reuse to link next released link in linkPool
+ nextDep: Link | undefined
+}
+
+export enum SubscriberFlags {
+ None = 0,
+ Tracking = 1 << 0,
+ CanPropagate = 1 << 1,
+ // RunInnerEffects = 1 << 2, // Not used in Vue
+ // 2~5 are using in EffectFlags
+ ToCheckDirty = 1 << 6,
+ Dirty = 1 << 7,
+ Dirtys = SubscriberFlags.ToCheckDirty | SubscriberFlags.Dirty,
+
+ DirtyFlagsIndex = 6,
+}
+
+let batchDepth = 0
+let queuedEffects: IEffect | undefined
+let queuedEffectsTail: IEffect | undefined
+let linkPool: Link | undefined
+
+export function startBatch(): void {
+ ++batchDepth
+}
+
+export function endBatch(): void {
+ if (!--batchDepth) {
+ drainQueuedEffects()
+ }
+}
+
+function drainQueuedEffects(): void {
+ while (queuedEffects !== undefined) {
+ const effect = queuedEffects
+ const queuedNext = effect.nextNotify
+ if (queuedNext !== undefined) {
+ effect.nextNotify = undefined
+ queuedEffects = queuedNext
+ } else {
+ queuedEffects = undefined
+ queuedEffectsTail = undefined
+ }
+ effect.notify()
+ }
+}
+
+export function link(dep: Dependency, sub: Subscriber): Link {
+ const currentDep = sub.depsTail
+ const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps
+ if (nextDep !== undefined && nextDep.dep === dep) {
+ sub.depsTail = nextDep
+ return nextDep
+ } else {
+ return linkNewDep(dep, sub, nextDep, currentDep)
+ }
+}
+
+function linkNewDep(
+ dep: Dependency,
+ sub: Subscriber,
+ nextDep: Link | undefined,
+ depsTail: Link | undefined,
+): Link {
+ let newLink: Link
+
+ if (linkPool !== undefined) {
+ newLink = linkPool
+ linkPool = newLink.nextDep
+ newLink.nextDep = nextDep
+ newLink.dep = dep
+ newLink.sub = sub
+ } else {
+ newLink = {
+ dep,
+ sub,
+ version: 0,
+ nextDep,
+ prevSub: undefined,
+ nextSub: undefined,
+ }
+ }
+
+ if (depsTail === undefined) {
+ sub.deps = newLink
+ } else {
+ depsTail.nextDep = newLink
+ }
+
+ if (dep.subs === undefined) {
+ dep.subs = newLink
+ } else {
+ const oldTail = dep.subsTail!
+ newLink.prevSub = oldTail
+ oldTail.nextSub = newLink
+ }
+
+ sub.depsTail = newLink
+ dep.subsTail = newLink
+
+ return newLink
+}
+
+export function propagate(subs: Link): void {
+ let targetFlag = SubscriberFlags.Dirty
+ let link = subs
+ let stack = 0
+ let nextSub: Link | undefined
+
+ top: do {
+ const sub = link.sub
+ const subFlags = sub.flags
+
+ if (!(subFlags & SubscriberFlags.Tracking)) {
+ let canPropagate = !(subFlags >> SubscriberFlags.DirtyFlagsIndex)
+ if (!canPropagate && subFlags & SubscriberFlags.CanPropagate) {
+ sub.flags &= ~SubscriberFlags.CanPropagate
+ canPropagate = true
+ }
+ if (canPropagate) {
+ sub.flags |= targetFlag
+ const subSubs = (sub as Dependency).subs
+ if (subSubs !== undefined) {
+ if (subSubs.nextSub !== undefined) {
+ subSubs.prevSub = subs
+ subs = subSubs
+ ++stack
+ }
+ link = subSubs
+ targetFlag = SubscriberFlags.ToCheckDirty
+ continue
+ }
+ if ('notify' in sub) {
+ if (queuedEffectsTail !== undefined) {
+ queuedEffectsTail.nextNotify = sub
+ } else {
+ queuedEffects = sub
+ }
+ queuedEffectsTail = sub
+ }
+ } else if (!(sub.flags & targetFlag)) {
+ sub.flags |= targetFlag
+ }
+ } else if (isValidLink(link, sub)) {
+ if (!(subFlags >> SubscriberFlags.DirtyFlagsIndex)) {
+ sub.flags |= targetFlag | SubscriberFlags.CanPropagate
+ const subSubs = (sub as Dependency).subs
+ if (subSubs !== undefined) {
+ if (subSubs.nextSub !== undefined) {
+ subSubs.prevSub = subs
+ subs = subSubs
+ ++stack
+ }
+ link = subSubs
+ targetFlag = SubscriberFlags.ToCheckDirty
+ continue
+ }
+ } else if (!(sub.flags & targetFlag)) {
+ sub.flags |= targetFlag
+ }
+ }
+
+ if ((nextSub = subs.nextSub) === undefined) {
+ if (stack) {
+ let dep = subs.dep
+ do {
+ --stack
+ const depSubs = dep.subs!
+ const prevLink = depSubs.prevSub!
+ depSubs.prevSub = undefined
+ link = subs = prevLink.nextSub!
+ if (subs !== undefined) {
+ targetFlag = stack
+ ? SubscriberFlags.ToCheckDirty
+ : SubscriberFlags.Dirty
+ continue top
+ }
+ dep = prevLink.dep
+ } while (stack)
+ }
+ break
+ }
+ if (link !== subs) {
+ targetFlag = stack ? SubscriberFlags.ToCheckDirty : SubscriberFlags.Dirty
+ }
+ link = subs = nextSub
+ } while (true)
+
+ if (!batchDepth) {
+ drainQueuedEffects()
+ }
+}
+
+function isValidLink(subLink: Link, sub: Subscriber) {
+ const depsTail = sub.depsTail
+ if (depsTail !== undefined) {
+ let link = sub.deps!
+ do {
+ if (link === subLink) {
+ return true
+ }
+ if (link === depsTail) {
+ break
+ }
+ link = link.nextDep!
+ } while (link !== undefined)
+ }
+ return false
+}
+
+export function checkDirty(deps: Link): boolean {
+ let stack = 0
+ let dirty: boolean
+ let nextDep: Link | undefined
+
+ top: do {
+ dirty = false
+ const dep = deps.dep
+ if ('update' in dep) {
+ if (dep.version !== deps.version) {
+ dirty = true
+ } else {
+ const depFlags = dep.flags
+ if (depFlags & SubscriberFlags.Dirty) {
+ dirty = dep.update()
+ } else if (depFlags & SubscriberFlags.ToCheckDirty) {
+ dep.subs!.prevSub = deps
+ deps = dep.deps!
+ ++stack
+ continue
+ }
+ }
+ }
+ if (dirty || (nextDep = deps.nextDep) === undefined) {
+ if (stack) {
+ let sub = deps.sub as IComputed
+ do {
+ --stack
+ const subSubs = sub.subs!
+ const prevLink = subSubs.prevSub!
+ subSubs.prevSub = undefined
+ if (dirty) {
+ if (sub.update()) {
+ sub = prevLink.sub as IComputed
+ dirty = true
+ continue
+ }
+ } else {
+ sub.flags &= ~SubscriberFlags.Dirtys
+ }
+ deps = prevLink.nextDep!
+ if (deps !== undefined) {
+ continue top
+ }
+ sub = prevLink.sub as IComputed
+ dirty = false
+ } while (stack)
+ }
+ return dirty
+ }
+ deps = nextDep
+ } while (true)
+}
+
+export function startTrack(sub: Subscriber): void {
+ sub.depsTail = undefined
+ sub.flags =
+ (sub.flags & ~(SubscriberFlags.CanPropagate | SubscriberFlags.Dirtys)) |
+ SubscriberFlags.Tracking
+}
+
+export function endTrack(sub: Subscriber): void {
+ const depsTail = sub.depsTail
+ if (depsTail !== undefined) {
+ if (depsTail.nextDep !== undefined) {
+ clearTrack(depsTail.nextDep)
+ depsTail.nextDep = undefined
+ }
+ } else if (sub.deps !== undefined) {
+ clearTrack(sub.deps)
+ sub.deps = undefined
+ }
+ sub.flags &= ~SubscriberFlags.Tracking
+}
+
+function clearTrack(link: Link): void {
+ do {
+ const dep = link.dep
+ const nextDep = link.nextDep
+ const nextSub = link.nextSub
+ const prevSub = link.prevSub
+
+ if (nextSub !== undefined) {
+ nextSub.prevSub = prevSub
+ link.nextSub = undefined
+ } else {
+ dep.subsTail = prevSub
+ if ('lastTrackedId' in dep) {
+ dep.lastTrackedId = 0
+ }
+ }
+
+ if (prevSub !== undefined) {
+ prevSub.nextSub = nextSub
+ link.prevSub = undefined
+ } else {
+ dep.subs = nextSub
+ }
+
+ // @ts-expect-error
+ link.dep = undefined
+ // @ts-expect-error
+ link.sub = undefined
+ link.nextDep = linkPool
+ linkPool = link
+
+ if (dep.subs === undefined && 'deps' in dep) {
+ if ('notify' in dep) {
+ dep.flags &= ~SubscriberFlags.Dirtys
+ } else {
+ dep.flags |= SubscriberFlags.Dirty
+ }
+ const depDeps = dep.deps
+ if (depDeps !== undefined) {
+ link = depDeps
+ dep.depsTail!.nextDep = nextDep
+ dep.deps = undefined
+ dep.depsTail = undefined
+ continue
+ }
+ }
+ link = nextDep!
+ } while (link !== undefined)
+}
isSet,
remove,
} from '@vue/shared'
-import { warn } from './warning'
import type { ComputedRef } from './computed'
import { ReactiveFlags } from './constants'
import {
type DebuggerOptions,
- EffectFlags,
type EffectScheduler,
ReactiveEffect,
pauseTracking,
resetTracking,
} from './effect'
+import { getCurrentScope } from './effectScope'
import { isReactive, isShallow } from './reactive'
import { type Ref, isRef } from './ref'
-import { getCurrentScope } from './effectScope'
+import { warn } from './warning'
// These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
// to @vue/reactivity to allow co-location with the moved base watch logic, hence
: INITIAL_WATCHER_VALUE
const job = (immediateFirstRun?: boolean) => {
- if (
- !(effect.flags & EffectFlags.ACTIVE) ||
- (!effect.dirty && !immediateFirstRun)
- ) {
+ if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
return
}
if (cb) {
+import {
+ type ComputedRefImpl,
+ type ReactiveEffectRunner,
+ effect,
+} from '@vue/reactivity'
import {
type ComponentInternalInstance,
type SetupContext,
withAsyncContext,
withDefaults,
} from '../src/apiSetupHelpers'
-import type { ComputedRefImpl } from '../../reactivity/src/computed'
-import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
describe('SFC <script setup> helpers', () => {
test('should warn runtime usage', () => {
app.mount(root)
await ready
- expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
- expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
+ expect(e!.effect.active).toBeTruthy()
+ expect(c!.flags & 1 /* SubscriberFlags.Tracking */).toBe(0)
app.unmount()
- expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
- expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
+ expect(e!.effect.active).toBeFalsy()
+ expect(c!.flags & 1 /* SubscriberFlags.Tracking */).toBe(0)
})
})
})
caughtError = caught
}
expect(fn).toHaveBeenCalledWith(err, 'setup function')
+ expect(
+ `Active effect was not restored correctly - this is likely a Vue internal bug.`,
+ ).toHaveBeenWarned()
expect(
`Unhandled error during execution of setup function`,
).toHaveBeenWarned()
instance.scope.off()
const update = (instance.update = effect.run.bind(effect))
- const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
+ const job: SchedulerJob = (instance.job = () =>
+ effect.dirty && effect.run())
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job)
export default defineConfig({
define: {
- __DEV__: true,
+ __DEV__: process.env.MODE !== 'benchmark',
__TEST__: true,
__VERSION__: '"test"',
__BROWSER__: false,
test: {
globals: true,
pool: 'threads',
+ poolOptions: {
+ forks: {
+ execArgv: ['--expose-gc'],
+ },
+ },
setupFiles: 'scripts/setup-vitest.ts',
environmentMatchGlobs: [
['packages/{vue,vue-compat,runtime-dom}/**', 'jsdom'],