| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- import { extend, hasChanged } from '@vue/shared'
- import type { ComputedRefImpl } from './computed'
- import type { TrackOpTypes, TriggerOpTypes } from './constants'
- import { type Link, globalVersion, targetMap } from './dep'
- import { activeEffectScope } from './effectScope'
- import { warn } from './warning'
- export type EffectScheduler = (...args: any[]) => any
- export type DebuggerEvent = {
- effect: Subscriber
- } & DebuggerEventExtraInfo
- export type DebuggerEventExtraInfo = {
- target: object
- type: TrackOpTypes | TriggerOpTypes
- key: any
- newValue?: any
- oldValue?: any
- oldTarget?: Map<any, any> | Set<any>
- }
- export interface DebuggerOptions {
- onTrack?: (event: DebuggerEvent) => void
- onTrigger?: (event: DebuggerEvent) => void
- }
- export interface ReactiveEffectOptions extends DebuggerOptions {
- scheduler?: EffectScheduler
- allowRecurse?: boolean
- onStop?: () => void
- }
- export interface ReactiveEffectRunner<T = any> {
- (): T
- effect: ReactiveEffect
- }
- export let activeSub: Subscriber | undefined
- export enum EffectFlags {
- /**
- * ReactiveEffect only
- */
- ACTIVE = 1 << 0,
- RUNNING = 1 << 1,
- TRACKING = 1 << 2,
- NOTIFIED = 1 << 3,
- DIRTY = 1 << 4,
- ALLOW_RECURSE = 1 << 5,
- PAUSED = 1 << 6,
- }
- /**
- * 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
- }
- const pausedQueueEffects = new WeakSet<ReactiveEffect>()
- 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
- constructor(public fn: () => T) {
- if (activeEffectScope && activeEffectScope.active) {
- activeEffectScope.effects.push(this)
- }
- }
- pause(): void {
- this.flags |= EffectFlags.PAUSED
- }
- resume(): void {
- if (this.flags & EffectFlags.PAUSED) {
- this.flags &= ~EffectFlags.PAUSED
- if (pausedQueueEffects.has(this)) {
- pausedQueueEffects.delete(this)
- this.trigger()
- }
- }
- }
- /**
- * @internal
- */
- notify(): void {
- if (
- this.flags & EffectFlags.RUNNING &&
- !(this.flags & EffectFlags.ALLOW_RECURSE)
- ) {
- return
- }
- if (!(this.flags & EffectFlags.NOTIFIED)) {
- batch(this)
- }
- }
- run(): T {
- // TODO cleanupEffect
- if (!(this.flags & EffectFlags.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
- try {
- return this.fn()
- } finally {
- if (__DEV__ && activeSub !== this) {
- warn(
- 'Active effect was not restored correctly - ' +
- 'this is likely a Vue internal bug.',
- )
- }
- cleanupDeps(this)
- activeSub = prevEffect
- shouldTrack = prevShouldTrack
- this.flags &= ~EffectFlags.RUNNING
- }
- }
- stop(): void {
- if (this.flags & EffectFlags.ACTIVE) {
- for (let link = this.deps; link; link = link.nextDep) {
- removeSub(link)
- }
- this.deps = this.depsTail = undefined
- 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()
- }
- }
- 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
- export function batch(sub: Subscriber): void {
- sub.flags |= EffectFlags.NOTIFIED
- 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
- }
- 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))
- ) {
- 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, fromComputed = false) {
- const { dep, prevSub, nextSub } = link
- if (prevSub) {
- prevSub.nextSub = nextSub
- link.prevSub = undefined
- }
- if (nextSub) {
- nextSub.prevSub = prevSub
- link.nextSub = undefined
- }
- if (dep.subs === link) {
- // was previous tail, point new tail to prev
- dep.subs = prevSub
- }
- if (__DEV__ && dep.subsHead === link) {
- // was previous head, point new head to next
- dep.subsHead = nextSub
- }
- if (!dep.subs) {
- // last subscriber removed
- if (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) {
- removeSub(l, true)
- }
- } else if (dep.map && !fromComputed) {
- // property dep, remove it from the owner depsMap
- dep.map.delete(dep.key)
- if (!dep.map.size) targetMap.delete(dep.target!)
- }
- }
- }
- function removeDep(link: Link) {
- const { prevDep, nextDep } = link
- if (prevDep) {
- prevDep.nextDep = nextDep
- link.prevDep = undefined
- }
- if (nextDep) {
- nextDep.prevDep = prevDep
- link.nextDep = undefined
- }
- }
- export interface ReactiveEffectRunner<T = any> {
- (): T
- effect: ReactiveEffect
- }
- export function effect<T = any>(
- fn: () => T,
- options?: ReactiveEffectOptions,
- ): ReactiveEffectRunner<T> {
- if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
- fn = (fn as ReactiveEffectRunner).effect.fn
- }
- const e = new ReactiveEffect(fn)
- if (options) {
- extend(e, options)
- }
- try {
- e.run()
- } catch (err) {
- e.stop()
- throw err
- }
- const runner = e.run.bind(e) as ReactiveEffectRunner
- runner.effect = e
- return runner
- }
- /**
- * Stops the effect associated with the given runner.
- *
- * @param runner - Association with the effect to stop tracking.
- */
- export function stop(runner: ReactiveEffectRunner): void {
- runner.effect.stop()
- }
- /**
- * @internal
- */
- export let shouldTrack = true
- const trackStack: boolean[] = []
- /**
- * Temporarily pauses tracking.
- */
- export function pauseTracking(): void {
- trackStack.push(shouldTrack)
- shouldTrack = false
- }
- /**
- * Re-enables effect tracking (if it was paused).
- */
- export function enableTracking(): void {
- trackStack.push(shouldTrack)
- shouldTrack = true
- }
- /**
- * Resets the previous global effect tracking state.
- */
- export function resetTracking(): void {
- const last = trackStack.pop()
- shouldTrack = last === undefined ? true : last
- }
- /**
- * Registers a cleanup function for the current active effect.
- * The cleanup function is called right before the next effect run, or when the
- * effect is stopped.
- *
- * Throws a warning if there is no current active effect. The warning can be
- * suppressed by passing `true` to the second argument.
- *
- * @param fn - the cleanup function to be registered
- * @param failSilently - if `true`, will not throw warning when called without
- * an active effect.
- */
- export function onEffectCleanup(fn: () => void, failSilently = false): void {
- if (activeSub instanceof ReactiveEffect) {
- activeSub.cleanup = fn
- } else if (__DEV__ && !failSilently) {
- warn(
- `onEffectCleanup() was called when there was no active effect` +
- ` to associate with.`,
- )
- }
- }
- function cleanupEffect(e: ReactiveEffect) {
- const { cleanup } = e
- e.cleanup = undefined
- if (cleanup) {
- // run cleanup without active effect
- const prevSub = activeSub
- activeSub = undefined
- try {
- cleanup()
- } finally {
- activeSub = prevSub
- }
- }
- }
|