} 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', () => {
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', () => {
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)', () => {
})
describe('dep unsubscribe', () => {
- function getSubCount(dep: Dependency | undefined) {
+ function getSubCount(dep: ReactiveNode | undefined) {
let count = 0
let sub = dep!.subs
while (sub) {
import {
type ComputedRef,
EffectScope,
+ ReactiveEffect,
computed,
effect,
effectScope,
onScopeDispose,
reactive,
ref,
+ setCurrentScope,
} from '../src'
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', () => {
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)
expect(dummy).toBe(7)
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
})
it('stop', () => {
effect(() => (doubled = counter.num * 2))
})
- expect(scope.effects.length).toBe(2)
+ expect(getEffectsCount(scope)).toBe(2)
expect(dummy).toBe(0)
counter.num = 7
})
})
- 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
})
})
- expect(scope.effects.length).toBe(1)
+ expect(getEffectsCount(scope)).toBe(1)
expect(dummy).toBe(0)
counter.num = 7
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)
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', () => {
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 () => {
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)
})
})
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
+}
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
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)
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:
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
}
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'
* @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
return this
}
// for backwards compat
- get dep(): Dependency {
+ get dep(): ReactiveNode {
return this
}
/**
*/
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
}
/**
*/
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)
}
}
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__) {
}
update(): boolean {
- const prevSub = activeSub
- setActiveSub(this)
- startTracking(this)
+ const prevSub = startTracking(this)
try {
const oldValue = this._value
const newValue = this.fn(oldValue)
}
return false
} finally {
- setActiveSub(prevSub)
- endTracking(this)
+ endTracking(this, prevSub)
}
}
}
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[] = []
})
}
-function setupFlagsHandler(target: Subscriber): void {
+function setupFlagsHandler(target: ReactiveNode): void {
;(target as any)._flags = target.flags
Object.defineProperty(target, 'flags', {
get() {
},
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)
}
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,
return
}
- const run = (dep: Dependency | undefined) => {
+ const run = (dep: ReactiveNode | undefined) => {
if (dep !== undefined && dep.subs !== undefined) {
if (__DEV__) {
triggerEventInfos.push({
})
}
propagate(dep.subs)
+ shallowPropagate(dep.subs)
if (__DEV__) {
triggerEventInfos.pop()
}
export function getDepFromReactive(
object: any,
key: string | number | symbol,
-): Dependency | undefined {
+): ReactiveNode | undefined {
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}
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 = {
*/
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
}
}
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 {
runner.effect.stop()
}
-const resetTrackingStack: (Subscriber | undefined)[] = []
+const resetTrackingStack: (ReactiveNode | undefined)[] = []
/**
* Temporarily pauses tracking.
*/
export function pauseTracking(): void {
resetTrackingStack.push(activeSub)
- activeSub = undefined
+ setActiveSub()
}
/**
resetTrackingStack.push(undefined)
for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
if (resetTrackingStack[i] !== undefined) {
- activeSub = resetTrackingStack[i]
+ setActiveSub(resetTrackingStack[i])
break
}
}
)
}
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
}
}
*/
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` +
}
}
-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
-}
-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()
- }
}
}
* 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)
}
}
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.
* @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` +
effectScope,
EffectScope,
getCurrentScope,
+ /**
+ * @internal
+ */
+ setCurrentScope,
onScopeDispose,
} from './effectScope'
export { reactiveReadArray, shallowReadArray } from './arrayInstrumentations'
traverse,
onWatcherCleanup,
WatchErrorCodes,
+ /**
+ * @internal
+ */
+ WatcherEffect,
type WatchOptions,
- type WatchScheduler,
type WatchStopHandle,
type WatchHandle,
type WatchEffect,
type WatchCallback,
type OnCleanup,
} from './watch'
+/**
+ * @internal
+ */
+export { setActiveSub } from './system'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { onTrack, triggerEventInfos } from './debug'
import { getDepFromReactive } from './dep'
-import { activeSub } from './effect'
import {
type Builtin,
type ShallowReactiveMarker,
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
/**
* @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
}
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))
+ }
}
/**
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!, {
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']
this._object[this._key] = newVal
}
- get dep(): Dependency | undefined {
+ get dep(): ReactiveNode | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}
/* 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
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
} 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!
}
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)
-}
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`
immediate?: Immediate
deep?: boolean | number
once?: boolean
- scheduler?: WatchScheduler
onWarn?: (msg: string, ...args: any[]) => void
- /**
- * @internal
- */
- augmentJob?: (job: (...args: any[]) => void) => void
/**
* @internal
*/
// 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.
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` +
}
}
-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(
} from '@vue/runtime-test'
import {
type DebuggerEvent,
+ type EffectScope,
ITERATE_KEY,
+ ReactiveEffect,
type Ref,
type ShallowRef,
TrackOpTypes,
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)
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 ', () => {
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 () => {
}
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(() => {
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
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
+}
const job1 = () => {
calls.push('job1')
- queueJob(job2)
- queueJob(job3)
+ queueJob(job2, 10)
+ queueJob(job3, 1)
}
const job2 = () => {
queueJob(job4)
queueJob(job5)
}
- job2.id = 10
const job3 = () => {
calls.push('job3')
}
- job3.id = 1
const job4 = () => {
calls.push('job4')
calls.push('cb1')
queueJob(job1)
}
- cb1.flags! |= SchedulerJobFlags.PRE
- queueJob(cb1)
+ queueJob(cb1, undefined, true)
await nextTick()
expect(calls).toEqual(['cb1', 'job1'])
})
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'])
})
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'])
// 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')
}
// 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()
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'])
})
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()
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()
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'])
})
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'])
})
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()
expect(job1).toHaveBeenCalledTimes(1)
expect(job2).toHaveBeenCalledTimes(0)
- queueJob(job1)
- queueJob(job2)
+ queueJob(job1, 1)
+ queueJob(job2, 2)
await nextTick()
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()
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()
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()
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()
spy()
flushPreFlushCbs()
}
- job.flags! |= SchedulerJobFlags.PRE
- queueJob(job)
+ queueJob(job, undefined, true)
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)
})
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()
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()
})
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'
(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) {
createSetupContext,
getCurrentGenericInstance,
setCurrentInstance,
- unsetCurrentInstance,
} from './component'
import type { EmitFn, EmitsOptions, ObjectEmitsOptions } from './componentEmits'
import type {
)
}
let awaitable = getAwaitable()
- unsetCurrentInstance()
+ setCurrentInstance(null, undefined)
if (isPromise(awaitable)) {
awaitable = awaitable.catch(e => {
setCurrentInstance(ctx)
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,
// 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)) {
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) {
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
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
}
TrackOpTypes,
isRef,
markRaw,
- pauseTracking,
proxyRefs,
- resetTracking,
+ setActiveSub,
shallowReadonly,
track,
} from '@vue/reactivity'
import {
setCurrentInstance,
setInSSRSetupState,
- unsetCurrentInstance,
} from './componentCurrentInstance'
export * from './componentCurrentInstance'
// 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,
],
)
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()
}
if (isAsyncSetup) {
+ const unsetCurrentInstance = (): void => {
+ setCurrentInstance(null, undefined)
+ }
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
// 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)
}
}
GenericComponentInstance,
} from './component'
import { currentRenderingInstance } from './componentRenderContext'
+import { type EffectScope, setCurrentScope } from '@vue/reactivity'
/**
* @internal
export let setInSSRSetupState: (state: boolean) => void
-let internalSetCurrentInstance: (
+/**
+ * @internal
+ */
+export let simpleSetCurrentInstance: (
instance: GenericComponentInstance | null,
) => void
else setters[0](v)
}
}
- internalSetCurrentInstance = registerGlobalSetter(
+ simpleSetCurrentInstance = registerGlobalSetter(
`__VUE_INSTANCE_SETTERS__`,
v => (currentInstance = v),
)
v => (isInSSRComponentSetup = v),
)
} else {
- internalSetCurrentInstance = i => {
+ simpleSetCurrentInstance = i => {
currentInstance = i
}
setInSSRSetupState = v => {
}
}
-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)
}
}
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)
: null,
props,
)
- reset()
+ setCurrentInstance(...prev)
return value
}
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
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
// 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))
}
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)
export function queueEffectWithSuspense(
fn: Function | Function[],
+ id: number | undefined,
suspense: SuspenseBoundary | null,
): void {
if (suspense && suspense.pendingBranch) {
suspense.effects.push(fn)
}
} else {
- queuePostFlushCb(fn)
+ queuePostFlushCb(fn, id)
}
}
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
isReadonly,
isRef,
isShallow,
- pauseTracking,
- resetTracking,
+ setActiveSub,
toRaw,
} from '@vue/reactivity'
import { EMPTY_OBJ, extend, isArray, isFunction, isObject } from '@vue/shared'
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',
{},
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,
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)
}
}
}
-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'
}
// app-level handling
if (errorHandler) {
- pauseTracking()
+ const prevSub = setActiveSub()
callWithErrorHandling(errorHandler, null, ErrorCodes.APP_ERROR_HANDLER, [
err,
exposedInstance,
errorInfo,
])
- resetTracking()
+ setActiveSub(prevSub)
return
}
}
} else {
const i = instance as ComponentInternalInstance
i.renderCache = []
- i.update()
+ i.effect.run()
}
nextTick(() => {
isHmrUpdating = false
if (parent.vapor) {
parent.hmrRerender!()
} else {
- ;(parent as ComponentInternalInstance).update()
+ ;(parent as ComponentInternalInstance).effect.run()
}
nextTick(() => {
isHmrUpdating = false
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,
+ )
}
}
*/
export {
currentInstance,
+ setCurrentInstance,
simpleSetCurrentInstance,
} from './componentCurrentInstance'
/**
import {
EffectFlags,
ReactiveEffect,
- pauseTracking,
- resetTracking,
+ setActiveSub,
+ setCurrentScope,
} from '@vue/reactivity'
import { updateProps } from './componentProps'
import { updateSlots } from './componentSlots'
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
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,
+ )
}
}
}
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,
+ )
}
}
// normal update
instance.next = n2
// instance.update is the reactive effect.
- instance.update()
+ instance.effect.run()
}
} else {
// no update needed. just copy over properties
}
}
- 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
}
// mounted hook
if (m) {
- queuePostRenderEffect(m, parentSuspense)
+ queuePostRenderEffect(m, undefined, parentSuspense)
}
// onVnodeMounted
if (
const scopedInitialVNode = initialVNode
queuePostRenderEffect(
() => invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode),
+ undefined,
parentSuspense,
)
}
) {
queuePostRenderEffect(
() => instance.emit('hook:mounted'),
+ undefined,
parentSuspense,
)
}
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,
)
}
}
// #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
nonHydratedAsyncRoot.asyncDep!.then(() => {
// the instance may be destroyed during the time period
if (!instance.isUnmounted) {
- componentUpdateFn()
+ this.fn()
}
})
return
}
// 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,
)
}
) {
queuePostRenderEffect(
() => instance.emit('hook:updated'),
+ undefined,
parentSuspense,
)
}
}
}
}
+ }
+ 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 = (
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 = (
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 = () => {
// 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
(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,
+ )
}
}
const {
bum,
scope,
- job,
+ effect,
subTree,
um,
m,
// 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__ &&
) {
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
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'
// #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()
}
-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.
* 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.
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>(
// 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 = () => {
}
}
-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()
}
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
}
}
}
}
- activePostFlushCbs = null
+
+ activePostJobs = null
postFlushIndex = 0
}
}
}
}
-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)
}
}
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'
// 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
console.warn(...warnArgs)
}
- resetTracking()
+ setActiveSub(prevSub)
isWarning = false
}
import {
+ type EffectScope,
+ ReactiveEffect,
currentInstance,
effectScope,
nextTick,
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 () => {
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
+}
import {
+ type EffectScope,
+ ReactiveEffect,
type Ref,
inject,
nextTick,
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', () => {
).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
+}
} 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()
isReactive,
isReadonly,
isShallow,
- pauseTracking,
- resetTracking,
+ setActiveSub,
shallowReadArray,
shallowRef,
toReactive,
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
- pauseTracking()
+ const prevSub = setActiveSub()
if (!isMounted) {
isMounted = true
frag.nodes.push(parentAnchor)
}
- resetTracking()
+ setActiveSub(prevSub)
}
const needKey = renderItem.length > 1
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(() => {
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 =
}
this.current = key
- pauseTracking()
+ const prevSub = setActiveSub()
const parent = this.anchor.parentNode
// teardown previous branch
parent && insert(this.nodes, parent, this.anchor)
}
- resetTracking()
+ setActiveSub(prevSub)
}
}
pushWarningContext,
queuePostFlushCb,
registerHMR,
- simpleSetCurrentInstance,
+ setCurrentInstance,
startMeasure,
unregisterHMR,
warn,
type ShallowRef,
markRaw,
onScopeDispose,
- pauseTracking,
proxyRefs,
- resetTracking,
+ setActiveSub,
unref,
} from '@vue/reactivity'
import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
instance.emitsOptions = normalizeEmitsOptions(component)
}
- const prev = currentInstance
- simpleSetCurrentInstance(instance)
- pauseTracking()
+ const prevInstance = setCurrentInstance(instance)
+ const prevSub = setActiveSub()
if (__DEV__) {
setupPropsValidation(instance)
}
}
- resetTracking()
- simpleSetCurrentInstance(prev, instance)
+ setActiveSub(prevSub)
+ setCurrentInstance(...prevInstance)
if (__DEV__) {
popWarningContext()
import {
type NormalizedPropsOptions,
baseNormalizePropsOptions,
- currentInstance,
isEmitListener,
popWarningContext,
pushWarningContext,
resolvePropValue,
- simpleSetCurrentInstance,
+ setCurrentInstance,
validateProps,
warn,
} from '@vue/runtime-dom'
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
}
import {
- currentInstance,
popWarningContext,
pushWarningContext,
- simpleSetCurrentInstance,
+ setCurrentInstance,
} from '@vue/runtime-dom'
import { insert, normalizeBlock, remove } from './block'
import {
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)
}
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)
}
-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()
}