perf(reactivity): refactor reactivity core by porting alien-signals (#12349)

This commit is contained in:
Johnson Chu 2024-12-02 21:05:12 +08:00 committed by GitHub
parent 6eb29d345a
commit 313dc61bef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 867 additions and 778 deletions

View File

@ -25,8 +25,9 @@ import {
toRaw,
triggerRef,
} from '../src'
import { EffectFlags, pauseTracking, resetTracking } from '../src/effect'
import type { ComputedRef, ComputedRefImpl } from '../src/computed'
import { pauseTracking, resetTracking } from '../src/effect'
import { SubscriberFlags } from '../src/system'
describe('reactivity/computed', () => {
it('should return updated value', () => {
@ -409,9 +410,9 @@ describe('reactivity/computed', () => {
a.value++
e.value
expect(e.deps!.dep).toBe(b.dep)
expect(e.deps!.nextDep!.dep).toBe(d.dep)
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c.dep)
expect(e.deps!.dep).toBe(b)
expect(e.deps!.nextDep!.dep).toBe(d)
expect(e.deps!.nextDep!.nextDep!.dep).toBe(c)
expect(cSpy).toHaveBeenCalledTimes(2)
a.value++
@ -466,8 +467,8 @@ describe('reactivity/computed', () => {
const c2 = computed(() => c1.value) as unknown as ComputedRefImpl
c2.value
expect(c1.flags & EffectFlags.DIRTY).toBeFalsy()
expect(c2.flags & EffectFlags.DIRTY).toBeFalsy()
expect(c1.flags & SubscriberFlags.Dirtys).toBe(0)
expect(c2.flags & SubscriberFlags.Dirtys).toBe(0)
})
it('should chained computeds dirtyLevel update with first computed effect', () => {

View File

@ -1,3 +1,14 @@
import {
computed,
h,
nextTick,
nodeOps,
ref,
render,
serializeInner,
} from '@vue/runtime-test'
import { ITERATE_KEY, getDepFromReactive } from '../src/dep'
import { onEffectCleanup, pauseTracking, resetTracking } from '../src/effect'
import {
type DebuggerEvent,
type ReactiveEffectRunner,
@ -11,23 +22,7 @@ import {
stop,
toRaw,
} from '../src/index'
import { type Dep, ITERATE_KEY, getDepFromReactive } from '../src/dep'
import {
computed,
h,
nextTick,
nodeOps,
ref,
render,
serializeInner,
} from '@vue/runtime-test'
import {
endBatch,
onEffectCleanup,
pauseTracking,
resetTracking,
startBatch,
} from '../src/effect'
import { type Dependency, endBatch, startBatch } from '../src/system'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
@ -1183,12 +1178,12 @@ describe('reactivity/effect', () => {
})
describe('dep unsubscribe', () => {
function getSubCount(dep: Dep | undefined) {
function getSubCount(dep: Dependency | undefined) {
let count = 0
let sub = dep!.subs
while (sub) {
count++
sub = sub.prevSub
sub = sub.nextSub
}
return count
}

View File

@ -2,6 +2,7 @@ import {
type ComputedRef,
computed,
effect,
effectScope,
reactive,
shallowRef as ref,
toRaw,
@ -19,7 +20,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
}
// #9233
it('should release computed cache', async () => {
it.todo('should release computed cache', async () => {
const src = ref<{} | undefined>({})
// @ts-expect-error ES2021 API
const srcRef = new WeakRef(src.value!)
@ -34,7 +35,7 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
expect(srcRef.deref()).toBeUndefined()
})
it('should release reactive property dep', async () => {
it.todo('should release reactive property dep', async () => {
const src = reactive({ foo: 1 })
let c: ComputedRef | undefined = computed(() => src.foo)
@ -79,4 +80,36 @@ describe.skipIf(!global.gc)('reactivity/gc', () => {
src.foo++
expect(spy).toHaveBeenCalledTimes(2)
})
it('should release computed that untrack by effect', async () => {
const src = ref(0)
// @ts-expect-error ES2021 API
const c = new WeakRef(computed(() => src.value))
const scope = effectScope()
scope.run(() => {
effect(() => c.deref().value)
})
expect(c.deref()).toBeDefined()
scope.stop()
await gc()
expect(c.deref()).toBeUndefined()
})
it('should release computed that untrack by effectScope', async () => {
const src = ref(0)
// @ts-expect-error ES2021 API
const c = new WeakRef(computed(() => src.value))
const scope = effectScope()
scope.run(() => {
c.deref().value
})
expect(c.deref()).toBeDefined()
scope.stop()
await gc()
expect(c.deref()).toBeUndefined()
})
})

View File

@ -1,8 +1,9 @@
import { TrackOpTypes } from './constants'
import { endBatch, pauseTracking, resetTracking, startBatch } from './effect'
import { isProxy, isShallow, toRaw, toReactive } from './reactive'
import { ARRAY_ITERATE_KEY, track } from './dep'
import { isArray } from '@vue/shared'
import { 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'
/**
* Track array iteration and return:

View File

@ -1,17 +1,27 @@
import { isFunction } from '@vue/shared'
import { hasChanged, isFunction } from '@vue/shared'
import { ReactiveFlags, TrackOpTypes } from './constants'
import { onTrack, setupFlagsHandler } from './debug'
import {
type DebuggerEvent,
type DebuggerOptions,
EffectFlags,
type Subscriber,
activeSub,
batch,
refreshComputed,
activeTrackId,
nextTrackId,
setActiveSub,
} from './effect'
import { activeEffectScope } from './effectScope'
import type { Ref } from './ref'
import {
type Dependency,
type IComputed,
type Link,
SubscriberFlags,
checkDirty,
endTrack,
link,
startTrack,
} from './system'
import { warn } from './warning'
import { Dep, type Link, globalVersion } from './dep'
import { ReactiveFlags, TrackOpTypes } from './constants'
declare const ComputedRefSymbol: unique symbol
declare const WritableComputedRefSymbol: unique symbol
@ -44,15 +54,23 @@ export interface WritableComputedOptions<T, S = T> {
* @private exported by @vue/reactivity for Vue core use, but not exported from
* the main vue package
*/
export class ComputedRefImpl<T = any> implements Subscriber {
export class ComputedRefImpl<T = any> implements IComputed {
/**
* @internal
*/
_value: any = undefined
/**
* @internal
*/
readonly dep: Dep = new Dep(this)
_value: T | undefined = undefined
version = 0
// Dependency
subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
lastTrackedId = 0
// Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: SubscriberFlags = SubscriberFlags.Dirty
/**
* @internal
*/
@ -63,34 +81,39 @@ export class ComputedRefImpl<T = any> implements Subscriber {
*/
readonly __v_isReadonly: boolean
// TODO isolatedDeclarations ReactiveFlags.IS_READONLY
// A computed is also a subscriber that tracks other deps
/**
* @internal
*/
deps?: Link = undefined
/**
* @internal
*/
depsTail?: Link = undefined
/**
* @internal
*/
flags: EffectFlags = EffectFlags.DIRTY
/**
* @internal
*/
globalVersion: number = globalVersion - 1
/**
* @internal
*/
isSSR: boolean
/**
* @internal
*/
next?: Subscriber = undefined
// for backwards compat
effect: this = this
get effect(): this {
return this
}
// for backwards compat
get dep(): Dependency {
return this
}
// for backwards compat
get _dirty(): boolean {
const flags = this.flags
if (flags & SubscriberFlags.Dirty) {
return true
} else if (flags & SubscriberFlags.ToCheckDirty) {
if (checkDirty(this.deps!)) {
this.flags |= SubscriberFlags.Dirty
return true
} else {
this.flags &= ~SubscriberFlags.ToCheckDirty
return false
}
}
return false
}
set _dirty(v: boolean) {
if (v) {
this.flags |= SubscriberFlags.Dirty
} else {
this.flags &= ~SubscriberFlags.Dirtys
}
}
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
@ -105,43 +128,34 @@ export class ComputedRefImpl<T = any> implements Subscriber {
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined,
isSSR: boolean,
) {
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}
/**
* @internal
*/
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
// avoid infinite self recursion
activeSub !== this
) {
batch(this, true)
return true
} else if (__DEV__) {
// TODO warn
if (__DEV__) {
setupFlagsHandler(this)
}
}
get value(): T {
const link = __DEV__
? this.dep.track({
if (this._dirty) {
this.update()
}
if (activeTrackId !== 0 && this.lastTrackedId !== activeTrackId) {
if (__DEV__) {
onTrack(activeSub!, {
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
: this.dep.track()
refreshComputed(this)
// sync version after evaluation
if (link) {
link.version = this.dep.version
}
this.lastTrackedId = activeTrackId
link(this, activeSub!).version = this.version
} else if (
activeEffectScope !== undefined &&
this.lastTrackedId !== activeEffectScope.trackId
) {
link(this, activeEffectScope)
}
return this._value
return this._value!
}
set value(newValue) {
@ -151,6 +165,27 @@ export class ComputedRefImpl<T = any> implements Subscriber {
warn('Write operation failed: computed value is readonly')
}
}
update(): boolean {
const prevSub = activeSub
const prevTrackId = activeTrackId
setActiveSub(this, nextTrackId())
startTrack(this)
const oldValue = this._value
let newValue: T
try {
newValue = this.fn(oldValue)
} finally {
setActiveSub(prevSub, prevTrackId)
endTrack(this)
}
if (hasChanged(oldValue, newValue)) {
this._value = newValue
this.version++
return true
}
return false
}
}
/**
@ -209,7 +244,7 @@ export function computed<T>(
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, isSSR)
const cRef = new ComputedRefImpl(getter, setter)
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack

View File

@ -0,0 +1,72 @@
import { extend } from '@vue/shared'
import type { DebuggerEventExtraInfo, ReactiveEffectOptions } from './effect'
import { type Link, type Subscriber, SubscriberFlags } from './system'
export const triggerEventInfos: DebuggerEventExtraInfo[] = []
export function onTrack(
sub: Link['sub'],
debugInfo: DebuggerEventExtraInfo,
): void {
if (!__DEV__) {
throw new Error(
`Internal error: onTrack should be called only in development.`,
)
}
if ((sub as ReactiveEffectOptions).onTrack) {
;(sub as ReactiveEffectOptions).onTrack!(
extend(
{
effect: sub,
},
debugInfo,
),
)
}
}
export function onTrigger(sub: Link['sub']): void {
if (!__DEV__) {
throw new Error(
`Internal error: onTrigger should be called only in development.`,
)
}
if ((sub as ReactiveEffectOptions).onTrigger) {
const debugInfo = triggerEventInfos[triggerEventInfos.length - 1]
;(sub as ReactiveEffectOptions).onTrigger!(
extend(
{
effect: sub,
},
debugInfo,
),
)
}
}
export function setupFlagsHandler(target: Subscriber): void {
if (!__DEV__) {
throw new Error(
`Internal error: setupFlagsHandler should be called only in development.`,
)
}
// @ts-expect-error
target._flags = target.flags
Object.defineProperty(target, 'flags', {
get() {
// @ts-expect-error
return target._flags
},
set(value) {
if (
// @ts-expect-error
!(target._flags >> SubscriberFlags.DirtyFlagsIndex) &&
!!(value >> SubscriberFlags.DirtyFlagsIndex)
) {
onTrigger(this)
}
// @ts-expect-error
target._flags = value
},
})
}

View File

@ -1,227 +1,35 @@
import { extend, isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared'
import { type TrackOpTypes, TriggerOpTypes } from './constants'
import { onTrack, triggerEventInfos } from './debug'
import { activeSub, activeTrackId } from './effect'
import {
type DebuggerEventExtraInfo,
EffectFlags,
type Subscriber,
activeSub,
type Dependency,
type Link,
endBatch,
shouldTrack,
link,
propagate,
startBatch,
} from './effect'
} from './system'
/**
* Incremented every time a reactive change happens
* This is used to give computed a fast path to avoid re-compute when nothing
* has changed.
*/
export let globalVersion = 0
/**
* Represents a link between a source (Dep) and a subscriber (Effect or Computed).
* Deps and subs have a many-to-many relationship - each link between a
* dep and a sub is represented by a Link instance.
*
* A Link is also a node in two doubly-linked lists - one for the associated
* sub to track all its deps, and one for the associated dep to track all its
* subs.
*
* @internal
*/
export class Link {
/**
* - Before each effect run, all previous dep links' version are reset to -1
* - During the run, a link's version is synced with the source dep on access
* - After the run, links with version -1 (that were never used) are cleaned
* up
*/
version: number
/**
* Pointers for doubly-linked lists
*/
nextDep?: Link
prevDep?: Link
nextSub?: Link
prevSub?: Link
prevActiveLink?: Link
class Dep implements Dependency {
_subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
lastTrackedId = 0
constructor(
public sub: Subscriber,
public dep: Dep,
) {
this.version = dep.version
this.nextDep =
this.prevDep =
this.nextSub =
this.prevSub =
this.prevActiveLink =
undefined
}
}
private map: KeyToDepMap,
private key: unknown,
) {}
/**
* @internal
*/
export class Dep {
version = 0
/**
* Link between this dep and the current active effect
*/
activeLink?: Link = undefined
/**
* Doubly linked list representing the subscribing effects (tail)
*/
subs?: Link = undefined
/**
* Doubly linked list representing the subscribing effects (head)
* DEV only, for invoking onTrigger hooks in correct order
*/
subsHead?: Link
/**
* For object property deps cleanup
*/
map?: KeyToDepMap = undefined
key?: unknown = undefined
/**
* Subscriber counter
*/
sc: number = 0
constructor(public computed?: ComputedRefImpl | undefined) {
if (__DEV__) {
this.subsHead = undefined
}
get subs(): Link | undefined {
return this._subs
}
track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
if (!activeSub || !shouldTrack || activeSub === this.computed) {
return
set subs(value: Link | undefined) {
this._subs = value
if (value === undefined) {
this.map.delete(this.key)
}
let link = this.activeLink
if (link === undefined || link.sub !== activeSub) {
link = this.activeLink = new Link(activeSub, this)
// add the link to the activeEffect as a dep (as tail)
if (!activeSub.deps) {
activeSub.deps = activeSub.depsTail = link
} else {
link.prevDep = activeSub.depsTail
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
}
addSub(link)
} else if (link.version === -1) {
// reused from last run - already a sub, just sync version
link.version = this.version
// If this dep has a next, it means it's not at the tail - move it to the
// tail. This ensures the effect's dep list is in the order they are
// accessed during evaluation.
if (link.nextDep) {
const next = link.nextDep
next.prevDep = link.prevDep
if (link.prevDep) {
link.prevDep.nextDep = next
}
link.prevDep = activeSub.depsTail
link.nextDep = undefined
activeSub.depsTail!.nextDep = link
activeSub.depsTail = link
// this was the head - point to the new head
if (activeSub.deps === link) {
activeSub.deps = next
}
}
}
if (__DEV__ && activeSub.onTrack) {
activeSub.onTrack(
extend(
{
effect: activeSub,
},
debugInfo,
),
)
}
return link
}
trigger(debugInfo?: DebuggerEventExtraInfo): void {
this.version++
globalVersion++
this.notify(debugInfo)
}
notify(debugInfo?: DebuggerEventExtraInfo): void {
startBatch()
try {
if (__DEV__) {
// subs are notified and batched in reverse-order and then invoked in
// original order at the end of the batch, but onTrigger hooks should
// be invoked in original order here.
for (let head = this.subsHead; head; head = head.nextSub) {
if (head.sub.onTrigger && !(head.sub.flags & EffectFlags.NOTIFIED)) {
head.sub.onTrigger(
extend(
{
effect: head.sub,
},
debugInfo,
),
)
}
}
}
for (let link = this.subs; link; link = link.prevSub) {
if (link.sub.notify()) {
// if notify() returns `true`, this is a computed. Also call notify
// on its dep - it's called here instead of inside computed's notify
// in order to reduce call stack depth.
;(link.sub as ComputedRefImpl).dep.notify()
}
}
} finally {
endBatch()
}
}
}
function addSub(link: Link) {
link.dep.sc++
if (link.sub.flags & EffectFlags.TRACKING) {
const computed = link.dep.computed
// computed getting its first subscriber
// enable tracking + lazily subscribe to all its deps
if (computed && !link.dep.subs) {
computed.flags |= EffectFlags.TRACKING | EffectFlags.DIRTY
for (let l = computed.deps; l; l = l.nextDep) {
addSub(l)
}
}
const currentTail = link.dep.subs
if (currentTail !== link) {
link.prevSub = currentTail
if (currentTail) currentTail.nextSub = link
}
if (__DEV__ && link.dep.subsHead === undefined) {
link.dep.subsHead = link
}
link.dep.subs = link
}
}
@ -254,25 +62,25 @@ export const ARRAY_ITERATE_KEY: unique symbol = Symbol(
* @param key - Identifier of the reactive property to track.
*/
export function track(target: object, type: TrackOpTypes, key: unknown): void {
if (shouldTrack && activeSub) {
if (activeTrackId > 0) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Dep()))
dep.map = depsMap
dep.key = key
depsMap.set(key, (dep = new Dep(depsMap, key)))
}
if (__DEV__) {
dep.track({
target,
type,
key,
})
} else {
dep.track()
if (dep.lastTrackedId !== activeTrackId) {
if (__DEV__) {
onTrack(activeSub!, {
target,
type,
key,
})
}
dep.lastTrackedId = activeTrackId
link(dep, activeSub!)
}
}
}
@ -296,14 +104,13 @@ export function trigger(
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
globalVersion++
return
}
const run = (dep: Dep | undefined) => {
if (dep) {
const run = (dep: Dependency | undefined) => {
if (dep !== undefined && dep.subs !== undefined) {
if (__DEV__) {
dep.trigger({
triggerEventInfos.push({
target,
type,
key,
@ -311,8 +118,10 @@ export function trigger(
oldValue,
oldTarget,
})
} else {
dep.trigger()
}
propagate(dep.subs)
if (__DEV__) {
triggerEventInfos.pop()
}
}
}
@ -385,7 +194,7 @@ export function trigger(
export function getDepFromReactive(
object: any,
key: string | number | symbol,
): Dep | undefined {
): Dependency | undefined {
const depMap = targetMap.get(object)
return depMap && depMap.get(key)
}

View File

@ -1,8 +1,16 @@
import { extend, hasChanged } from '@vue/shared'
import type { ComputedRefImpl } from './computed'
import { extend } from '@vue/shared'
import type { TrackOpTypes, TriggerOpTypes } from './constants'
import { type Link, globalVersion } from './dep'
import { setupFlagsHandler } from './debug'
import { activeEffectScope } from './effectScope'
import {
type IEffect,
type Link,
type Subscriber,
SubscriberFlags,
checkDirty,
endTrack,
startTrack,
} from './system'
import { warn } from './warning'
export type EffectScheduler = (...args: any[]) => any
@ -27,7 +35,6 @@ export interface DebuggerOptions {
export interface ReactiveEffectOptions extends DebuggerOptions {
scheduler?: EffectScheduler
allowRecurse?: boolean
onStop?: () => void
}
@ -36,78 +43,29 @@ export interface ReactiveEffectRunner<T = any> {
effect: ReactiveEffect
}
export let activeSub: Subscriber | undefined
export enum EffectFlags {
/**
* ReactiveEffect only
*/
ACTIVE = 1 << 0,
RUNNING = 1 << 1,
TRACKING = 1 << 2,
NOTIFIED = 1 << 3,
DIRTY = 1 << 4,
ALLOW_RECURSE = 1 << 5,
PAUSED = 1 << 6,
ALLOW_RECURSE = 1 << 2,
PAUSED = 1 << 3,
NOTIFIED = 1 << 4,
STOP = 1 << 5,
}
/**
* Subscriber is a type that tracks (or subscribes to) a list of deps.
*/
export interface Subscriber extends DebuggerOptions {
/**
* Head of the doubly linked list representing the deps
* @internal
*/
deps?: Link
/**
* Tail of the same list
* @internal
*/
depsTail?: Link
/**
* @internal
*/
flags: EffectFlags
/**
* @internal
*/
next?: Subscriber
/**
* returning `true` indicates it's a computed that needs to call notify
* on its dep too
* @internal
*/
notify(): true | void
}
export class ReactiveEffect<T = any> implements IEffect, ReactiveEffectOptions {
nextNotify: IEffect | undefined = undefined
const pausedQueueEffects = new WeakSet<ReactiveEffect>()
// Subscriber
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: number = SubscriberFlags.Dirty
export class ReactiveEffect<T = any>
implements Subscriber, ReactiveEffectOptions
{
/**
* @internal
*/
deps?: Link = undefined
/**
* @internal
*/
depsTail?: Link = undefined
/**
* @internal
*/
flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING
/**
* @internal
*/
next?: Subscriber = undefined
/**
* @internal
*/
cleanup?: () => void = undefined
scheduler?: EffectScheduler = undefined
onStop?: () => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
@ -116,52 +74,59 @@ export class ReactiveEffect<T = any>
if (activeEffectScope && activeEffectScope.active) {
activeEffectScope.effects.push(this)
}
if (__DEV__) {
setupFlagsHandler(this)
}
}
get active(): boolean {
return !(this.flags & EffectFlags.STOP)
}
pause(): void {
this.flags |= EffectFlags.PAUSED
if (!(this.flags & EffectFlags.PAUSED)) {
this.flags |= EffectFlags.PAUSED
}
}
resume(): void {
if (this.flags & EffectFlags.PAUSED) {
const flags = this.flags
if (flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
if (pausedQueueEffects.has(this)) {
pausedQueueEffects.delete(this)
this.trigger()
}
}
if (flags & EffectFlags.NOTIFIED) {
this.flags &= ~EffectFlags.NOTIFIED
this.notify()
}
}
/**
* @internal
*/
notify(): void {
if (
this.flags & EffectFlags.RUNNING &&
!(this.flags & EffectFlags.ALLOW_RECURSE)
) {
return
const flags = this.flags
if (!(flags & EffectFlags.PAUSED)) {
this.scheduler()
} else {
this.flags |= EffectFlags.NOTIFIED
}
if (!(this.flags & EffectFlags.NOTIFIED)) {
batch(this)
}
scheduler(): void {
if (this.dirty) {
this.run()
}
}
run(): T {
// TODO cleanupEffect
if (!(this.flags & EffectFlags.ACTIVE)) {
if (!this.active) {
// stopped during cleanup
return this.fn()
}
this.flags |= EffectFlags.RUNNING
cleanupEffect(this)
prepareDeps(this)
const prevEffect = activeSub
const prevShouldTrack = shouldTrack
activeSub = this
shouldTrack = true
const prevSub = activeSub
const prevTrackId = activeTrackId
setActiveSub(this, nextTrackId())
startTrack(this)
try {
return this.fn()
@ -172,299 +137,42 @@ export class ReactiveEffect<T = any>
'this is likely a Vue internal bug.',
)
}
cleanupDeps(this)
activeSub = prevEffect
shouldTrack = prevShouldTrack
this.flags &= ~EffectFlags.RUNNING
setActiveSub(prevSub, prevTrackId)
endTrack(this)
if (
this.flags & SubscriberFlags.CanPropagate &&
this.flags & EffectFlags.ALLOW_RECURSE
) {
this.flags &= ~SubscriberFlags.CanPropagate
this.notify()
}
}
}
stop(): void {
if (this.flags & EffectFlags.ACTIVE) {
for (let link = this.deps; link; link = link.nextDep) {
removeSub(link)
}
this.deps = this.depsTail = undefined
if (this.active) {
startTrack(this)
endTrack(this)
cleanupEffect(this)
this.onStop && this.onStop()
this.flags &= ~EffectFlags.ACTIVE
}
}
trigger(): void {
if (this.flags & EffectFlags.PAUSED) {
pausedQueueEffects.add(this)
} else if (this.scheduler) {
this.scheduler()
} else {
this.runIfDirty()
}
}
/**
* @internal
*/
runIfDirty(): void {
if (isDirty(this)) {
this.run()
this.flags |= EffectFlags.STOP
}
}
get dirty(): boolean {
return isDirty(this)
}
}
/**
* For debugging
*/
// function printDeps(sub: Subscriber) {
// let d = sub.deps
// let ds = []
// while (d) {
// ds.push(d)
// d = d.nextDep
// }
// return ds.map(d => ({
// id: d.id,
// prev: d.prevDep?.id,
// next: d.nextDep?.id,
// }))
// }
let batchDepth = 0
let batchedSub: Subscriber | undefined
let batchedComputed: Subscriber | undefined
export function batch(sub: Subscriber, isComputed = false): void {
sub.flags |= EffectFlags.NOTIFIED
if (isComputed) {
sub.next = batchedComputed
batchedComputed = sub
return
}
sub.next = batchedSub
batchedSub = sub
}
/**
* @internal
*/
export function startBatch(): void {
batchDepth++
}
/**
* Run batched effects when all batches have ended
* @internal
*/
export function endBatch(): void {
if (--batchDepth > 0) {
return
}
if (batchedComputed) {
let e: Subscriber | undefined = batchedComputed
batchedComputed = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
e = next
}
}
let error: unknown
while (batchedSub) {
let e: Subscriber | undefined = batchedSub
batchedSub = undefined
while (e) {
const next: Subscriber | undefined = e.next
e.next = undefined
e.flags &= ~EffectFlags.NOTIFIED
if (e.flags & EffectFlags.ACTIVE) {
try {
// ACTIVE flag is effect-only
;(e as ReactiveEffect).trigger()
} catch (err) {
if (!error) error = err
}
}
e = next
}
}
if (error) throw error
}
function prepareDeps(sub: Subscriber) {
// Prepare deps for tracking, starting from the head
for (let link = sub.deps; link; link = link.nextDep) {
// set all previous deps' (if any) version to -1 so that we can track
// which ones are unused after the run
link.version = -1
// store previous active sub if link was being used in another context
link.prevActiveLink = link.dep.activeLink
link.dep.activeLink = link
}
}
function cleanupDeps(sub: Subscriber) {
// Cleanup unsued deps
let head
let tail = sub.depsTail
let link = tail
while (link) {
const prev = link.prevDep
if (link.version === -1) {
if (link === tail) tail = prev
// unused - remove it from the dep's subscribing effect list
removeSub(link)
// also remove it from this effect's dep list
removeDep(link)
} else {
// The new head is the last node seen which wasn't removed
// from the doubly-linked list
head = link
}
// restore previous active link if any
link.dep.activeLink = link.prevActiveLink
link.prevActiveLink = undefined
link = prev
}
// set the new head & tail
sub.deps = head
sub.depsTail = tail
}
function isDirty(sub: Subscriber): boolean {
for (let link = sub.deps; link; link = link.nextDep) {
if (
link.dep.version !== link.version ||
(link.dep.computed &&
(refreshComputed(link.dep.computed) ||
link.dep.version !== link.version))
) {
const flags = this.flags
if (flags & SubscriberFlags.Dirty) {
return true
}
}
// @ts-expect-error only for backwards compatibility where libs manually set
// this flag - e.g. Pinia's testing module
if (sub._dirty) {
return true
}
return false
}
/**
* Returning false indicates the refresh failed
* @internal
*/
export function refreshComputed(computed: ComputedRefImpl): undefined {
if (
computed.flags & EffectFlags.TRACKING &&
!(computed.flags & EffectFlags.DIRTY)
) {
return
}
computed.flags &= ~EffectFlags.DIRTY
// Global version fast path when no reactive changes has happened since
// last refresh.
if (computed.globalVersion === globalVersion) {
return
}
computed.globalVersion = globalVersion
const dep = computed.dep
computed.flags |= EffectFlags.RUNNING
// In SSR there will be no render effect, so the computed has no subscriber
// and therefore tracks no deps, thus we cannot rely on the dirty check.
// Instead, computed always re-evaluate and relies on the globalVersion
// fast path above for caching.
if (
dep.version > 0 &&
!computed.isSSR &&
computed.deps &&
!isDirty(computed)
) {
computed.flags &= ~EffectFlags.RUNNING
return
}
const prevSub = activeSub
const prevShouldTrack = shouldTrack
activeSub = computed
shouldTrack = true
try {
prepareDeps(computed)
const value = computed.fn(computed._value)
if (dep.version === 0 || hasChanged(value, computed._value)) {
computed._value = value
dep.version++
}
} catch (err) {
dep.version++
throw err
} finally {
activeSub = prevSub
shouldTrack = prevShouldTrack
cleanupDeps(computed)
computed.flags &= ~EffectFlags.RUNNING
}
}
function removeSub(link: Link, soft = false) {
const { dep, prevSub, nextSub } = link
if (prevSub) {
prevSub.nextSub = nextSub
link.prevSub = undefined
}
if (nextSub) {
nextSub.prevSub = prevSub
link.nextSub = undefined
}
if (__DEV__ && dep.subsHead === link) {
// was previous head, point new head to next
dep.subsHead = nextSub
}
if (dep.subs === link) {
// was previous tail, point new tail to prev
dep.subs = prevSub
if (!prevSub && dep.computed) {
// if computed, unsubscribe it from all its deps so this computed and its
// value can be GCed
dep.computed.flags &= ~EffectFlags.TRACKING
for (let l = dep.computed.deps; l; l = l.nextDep) {
// here we are only "soft" unsubscribing because the computed still keeps
// referencing the deps and the dep should not decrease its sub count
removeSub(l, true)
} else if (flags & SubscriberFlags.ToCheckDirty) {
if (checkDirty(this.deps!)) {
this.flags |= SubscriberFlags.Dirty
return true
} else {
this.flags &= ~SubscriberFlags.ToCheckDirty
return false
}
}
}
if (!soft && !--dep.sc && dep.map) {
// #11979
// property dep no longer has effect subscribers, delete it
// this mostly is for the case where an object is kept in memory but only a
// subset of its properties is tracked at one time
dep.map.delete(dep.key)
}
}
function removeDep(link: Link) {
const { prevDep, nextDep } = link
if (prevDep) {
prevDep.nextDep = nextDep
link.prevDep = undefined
}
if (nextDep) {
nextDep.prevDep = prevDep
link.nextDep = undefined
return false
}
}
@ -505,34 +213,55 @@ export function stop(runner: ReactiveEffectRunner): void {
runner.effect.stop()
}
/**
* @internal
*/
export let shouldTrack = true
const trackStack: boolean[] = []
const resetTrackingStack: [sub: typeof activeSub, trackId: number][] = []
/**
* Temporarily pauses tracking.
*/
export function pauseTracking(): void {
trackStack.push(shouldTrack)
shouldTrack = false
resetTrackingStack.push([activeSub, activeTrackId])
activeSub = undefined
activeTrackId = 0
}
/**
* Re-enables effect tracking (if it was paused).
*/
export function enableTracking(): void {
trackStack.push(shouldTrack)
shouldTrack = true
const isPaused = activeSub === undefined
if (!isPaused) {
// Add the current active effect to the trackResetStack so it can be
// restored by calling resetTracking.
resetTrackingStack.push([activeSub, activeTrackId])
} else {
// Add a placeholder to the trackResetStack so we can it can be popped
// to restore the previous active effect.
resetTrackingStack.push([undefined, 0])
for (let i = resetTrackingStack.length - 1; i >= 0; i--) {
if (resetTrackingStack[i][0] !== undefined) {
;[activeSub, activeTrackId] = resetTrackingStack[i]
break
}
}
}
}
/**
* Resets the previous global effect tracking state.
*/
export function resetTracking(): void {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
if (__DEV__ && resetTrackingStack.length === 0) {
warn(
`resetTracking() was called when there was no active tracking ` +
`to reset.`,
)
}
if (resetTrackingStack.length) {
;[activeSub, activeTrackId] = resetTrackingStack.pop()!
} else {
activeSub = undefined
activeTrackId = 0
}
}
/**
@ -561,7 +290,7 @@ export function onEffectCleanup(fn: () => void, failSilently = false): void {
function cleanupEffect(e: ReactiveEffect) {
const { cleanup } = e
e.cleanup = undefined
if (cleanup) {
if (cleanup !== undefined) {
// run cleanup without active effect
const prevSub = activeSub
activeSub = undefined
@ -572,3 +301,16 @@ function cleanupEffect(e: ReactiveEffect) {
}
}
}
export let activeSub: Subscriber | undefined = undefined
export let activeTrackId = 0
export let lastTrackId = 0
export const nextTrackId = (): number => ++lastTrackId
export function setActiveSub(
sub: Subscriber | undefined,
trackId: number,
): void {
activeSub = sub
activeTrackId = trackId
}

View File

@ -1,13 +1,23 @@
import type { ReactiveEffect } from './effect'
import { EffectFlags, type ReactiveEffect, nextTrackId } from './effect'
import {
type Link,
type Subscriber,
SubscriberFlags,
endTrack,
startTrack,
} from './system'
import { warn } from './warning'
export let activeEffectScope: EffectScope | undefined
export class EffectScope {
/**
* @internal
*/
private _active = true
export class EffectScope implements Subscriber {
// Subscriber: In order to collect orphans computeds
deps: Link | undefined = undefined
depsTail: Link | undefined = undefined
flags: number = SubscriberFlags.None
trackId: number = nextTrackId()
/**
* @internal
*/
@ -17,8 +27,6 @@ export class EffectScope {
*/
cleanups: (() => void)[] = []
private _isPaused = false
/**
* only assigned by undetached scope
* @internal
@ -47,12 +55,12 @@ export class EffectScope {
}
get active(): boolean {
return this._active
return !(this.flags & EffectFlags.STOP)
}
pause(): void {
if (this._active) {
this._isPaused = true
if (!(this.flags & EffectFlags.PAUSED)) {
this.flags |= EffectFlags.PAUSED
let i, l
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
@ -69,24 +77,22 @@ export class EffectScope {
* Resumes the effect scope, including all child scopes and effects.
*/
resume(): void {
if (this._active) {
if (this._isPaused) {
this._isPaused = false
let i, l
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].resume()
}
}
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].resume()
if (this.flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
let i, l
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].resume()
}
}
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].resume()
}
}
}
run<T>(fn: () => T): T | undefined {
if (this._active) {
if (this.active) {
const currentEffectScope = activeEffectScope
try {
activeEffectScope = this
@ -116,8 +122,10 @@ export class EffectScope {
}
stop(fromParent?: boolean): void {
if (this._active) {
this._active = false
if (this.active) {
this.flags |= EffectFlags.STOP
startTrack(this)
endTrack(this)
let i, l
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()

View File

@ -5,7 +5,11 @@ import {
isFunction,
isObject,
} from '@vue/shared'
import { Dep, getDepFromReactive } from './dep'
import type { ComputedRef, WritableComputedRef } from './computed'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { onTrack, triggerEventInfos } from './debug'
import { getDepFromReactive } from './dep'
import { activeSub, activeTrackId } from './effect'
import {
type Builtin,
type ShallowReactiveMarker,
@ -16,8 +20,7 @@ import {
toRaw,
toReactive,
} from './reactive'
import type { ComputedRef, WritableComputedRef } from './computed'
import { ReactiveFlags, TrackOpTypes, TriggerOpTypes } from './constants'
import { type Dependency, type Link, link, propagate } from './system'
import { warn } from './warning'
declare const RefSymbol: unique symbol
@ -105,12 +108,15 @@ function createRef(rawValue: unknown, shallow: boolean) {
/**
* @internal
*/
class RefImpl<T = any> {
class RefImpl<T = any> implements Dependency {
// Dependency
subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
lastTrackedId = 0
_value: T
private _rawValue: T
dep: Dep = new Dep()
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
@ -120,16 +126,12 @@ class RefImpl<T = any> {
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get dep() {
return this
}
get value() {
if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
} else {
this.dep.track()
}
trackRef(this)
return this._value
}
@ -144,15 +146,17 @@ class RefImpl<T = any> {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
if (__DEV__) {
this.dep.trigger({
triggerEventInfos.push({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
} else {
this.dep.trigger()
}
triggerRef(this as unknown as Ref)
if (__DEV__) {
triggerEventInfos.pop()
}
}
}
@ -185,17 +189,23 @@ class RefImpl<T = any> {
*/
export function triggerRef(ref: Ref): void {
// ref may be an instance of ObjectRefImpl
if ((ref as unknown as RefImpl).dep) {
const dep = (ref as unknown as RefImpl).dep
if (dep !== undefined && dep.subs !== undefined) {
propagate(dep.subs)
}
}
function trackRef(dep: Dependency) {
if (activeTrackId !== 0 && dep.lastTrackedId !== activeTrackId) {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
onTrack(activeSub!, {
target: dep,
type: TrackOpTypes.GET,
key: 'value',
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
}
dep.lastTrackedId = activeTrackId
link(dep, activeSub!)
}
}
@ -287,8 +297,11 @@ export type CustomRefFactory<T> = (
set: (value: T) => void
}
class CustomRefImpl<T> {
public dep: Dep
class CustomRefImpl<T> implements Dependency {
// Dependency
subs: Link | undefined = undefined
subsTail: Link | undefined = undefined
lastTrackedId = 0
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
@ -298,12 +311,18 @@ class CustomRefImpl<T> {
public _value: T = undefined!
constructor(factory: CustomRefFactory<T>) {
const dep = (this.dep = new Dep())
const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep))
const { get, set } = factory(
() => trackRef(this),
() => triggerRef(this as unknown as Ref),
)
this._get = get
this._set = set
}
get dep() {
return this
}
get value() {
return (this._value = this._get())
}
@ -366,7 +385,7 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
this._object[this._key] = newVal
}
get dep(): Dep | undefined {
get dep(): Dependency | undefined {
return getDepFromReactive(toRaw(this._object), this._key)
}
}

View File

@ -0,0 +1,366 @@
// Ported from https://github.com/stackblitz/alien-signals/blob/v0.4.4/src/system.ts
export interface IEffect extends Subscriber {
nextNotify: IEffect | undefined
notify(): void
}
export interface IComputed extends Dependency, Subscriber {
version: number
update(): boolean
}
export interface Dependency {
subs: Link | undefined
subsTail: Link | undefined
lastTrackedId?: number
}
export interface Subscriber {
flags: SubscriberFlags
deps: Link | undefined
depsTail: Link | undefined
}
export interface Link {
dep: Dependency | IComputed | (Dependency & IEffect)
sub: Subscriber | IComputed | (Dependency & IEffect) | IEffect
version: number
// Reuse to link prev stack in checkDirty
// Reuse to link prev stack in propagate
prevSub: Link | undefined
nextSub: Link | undefined
// Reuse to link next released link in linkPool
nextDep: Link | undefined
}
export enum SubscriberFlags {
None = 0,
Tracking = 1 << 0,
CanPropagate = 1 << 1,
// RunInnerEffects = 1 << 2, // Not used in Vue
// 2~5 are using in EffectFlags
ToCheckDirty = 1 << 6,
Dirty = 1 << 7,
Dirtys = SubscriberFlags.ToCheckDirty | SubscriberFlags.Dirty,
DirtyFlagsIndex = 6,
}
let batchDepth = 0
let queuedEffects: IEffect | undefined
let queuedEffectsTail: IEffect | undefined
let linkPool: Link | undefined
export function startBatch(): void {
++batchDepth
}
export function endBatch(): void {
if (!--batchDepth) {
drainQueuedEffects()
}
}
function drainQueuedEffects(): void {
while (queuedEffects !== undefined) {
const effect = queuedEffects
const queuedNext = effect.nextNotify
if (queuedNext !== undefined) {
effect.nextNotify = undefined
queuedEffects = queuedNext
} else {
queuedEffects = undefined
queuedEffectsTail = undefined
}
effect.notify()
}
}
export function link(dep: Dependency, sub: Subscriber): Link {
const currentDep = sub.depsTail
const nextDep = currentDep !== undefined ? currentDep.nextDep : sub.deps
if (nextDep !== undefined && nextDep.dep === dep) {
sub.depsTail = nextDep
return nextDep
} else {
return linkNewDep(dep, sub, nextDep, currentDep)
}
}
function linkNewDep(
dep: Dependency,
sub: Subscriber,
nextDep: Link | undefined,
depsTail: Link | undefined,
): Link {
let newLink: Link
if (linkPool !== undefined) {
newLink = linkPool
linkPool = newLink.nextDep
newLink.nextDep = nextDep
newLink.dep = dep
newLink.sub = sub
} else {
newLink = {
dep,
sub,
version: 0,
nextDep,
prevSub: undefined,
nextSub: undefined,
}
}
if (depsTail === undefined) {
sub.deps = newLink
} else {
depsTail.nextDep = newLink
}
if (dep.subs === undefined) {
dep.subs = newLink
} else {
const oldTail = dep.subsTail!
newLink.prevSub = oldTail
oldTail.nextSub = newLink
}
sub.depsTail = newLink
dep.subsTail = newLink
return newLink
}
export function propagate(subs: Link): void {
let targetFlag = SubscriberFlags.Dirty
let link = subs
let stack = 0
let nextSub: Link | undefined
top: do {
const sub = link.sub
const subFlags = sub.flags
if (!(subFlags & SubscriberFlags.Tracking)) {
let canPropagate = !(subFlags >> SubscriberFlags.DirtyFlagsIndex)
if (!canPropagate && subFlags & SubscriberFlags.CanPropagate) {
sub.flags &= ~SubscriberFlags.CanPropagate
canPropagate = true
}
if (canPropagate) {
sub.flags |= targetFlag
const subSubs = (sub as Dependency).subs
if (subSubs !== undefined) {
if (subSubs.nextSub !== undefined) {
subSubs.prevSub = subs
subs = subSubs
++stack
}
link = subSubs
targetFlag = SubscriberFlags.ToCheckDirty
continue
}
if ('notify' in sub) {
if (queuedEffectsTail !== undefined) {
queuedEffectsTail.nextNotify = sub
} else {
queuedEffects = sub
}
queuedEffectsTail = sub
}
} else if (!(sub.flags & targetFlag)) {
sub.flags |= targetFlag
}
} else if (isValidLink(link, sub)) {
if (!(subFlags >> SubscriberFlags.DirtyFlagsIndex)) {
sub.flags |= targetFlag | SubscriberFlags.CanPropagate
const subSubs = (sub as Dependency).subs
if (subSubs !== undefined) {
if (subSubs.nextSub !== undefined) {
subSubs.prevSub = subs
subs = subSubs
++stack
}
link = subSubs
targetFlag = SubscriberFlags.ToCheckDirty
continue
}
} else if (!(sub.flags & targetFlag)) {
sub.flags |= targetFlag
}
}
if ((nextSub = subs.nextSub) === undefined) {
if (stack) {
let dep = subs.dep
do {
--stack
const depSubs = dep.subs!
const prevLink = depSubs.prevSub!
depSubs.prevSub = undefined
link = subs = prevLink.nextSub!
if (subs !== undefined) {
targetFlag = stack
? SubscriberFlags.ToCheckDirty
: SubscriberFlags.Dirty
continue top
}
dep = prevLink.dep
} while (stack)
}
break
}
if (link !== subs) {
targetFlag = stack ? SubscriberFlags.ToCheckDirty : SubscriberFlags.Dirty
}
link = subs = nextSub
} while (true)
if (!batchDepth) {
drainQueuedEffects()
}
}
function isValidLink(subLink: Link, sub: Subscriber) {
const depsTail = sub.depsTail
if (depsTail !== undefined) {
let link = sub.deps!
do {
if (link === subLink) {
return true
}
if (link === depsTail) {
break
}
link = link.nextDep!
} while (link !== undefined)
}
return false
}
export function checkDirty(deps: Link): boolean {
let stack = 0
let dirty: boolean
let nextDep: Link | undefined
top: do {
dirty = false
const dep = deps.dep
if ('update' in dep) {
if (dep.version !== deps.version) {
dirty = true
} else {
const depFlags = dep.flags
if (depFlags & SubscriberFlags.Dirty) {
dirty = dep.update()
} else if (depFlags & SubscriberFlags.ToCheckDirty) {
dep.subs!.prevSub = deps
deps = dep.deps!
++stack
continue
}
}
}
if (dirty || (nextDep = deps.nextDep) === undefined) {
if (stack) {
let sub = deps.sub as IComputed
do {
--stack
const subSubs = sub.subs!
const prevLink = subSubs.prevSub!
subSubs.prevSub = undefined
if (dirty) {
if (sub.update()) {
sub = prevLink.sub as IComputed
dirty = true
continue
}
} else {
sub.flags &= ~SubscriberFlags.Dirtys
}
deps = prevLink.nextDep!
if (deps !== undefined) {
continue top
}
sub = prevLink.sub as IComputed
dirty = false
} while (stack)
}
return dirty
}
deps = nextDep
} while (true)
}
export function startTrack(sub: Subscriber): void {
sub.depsTail = undefined
sub.flags =
(sub.flags & ~(SubscriberFlags.CanPropagate | SubscriberFlags.Dirtys)) |
SubscriberFlags.Tracking
}
export function endTrack(sub: Subscriber): void {
const depsTail = sub.depsTail
if (depsTail !== undefined) {
if (depsTail.nextDep !== undefined) {
clearTrack(depsTail.nextDep)
depsTail.nextDep = undefined
}
} else if (sub.deps !== undefined) {
clearTrack(sub.deps)
sub.deps = undefined
}
sub.flags &= ~SubscriberFlags.Tracking
}
function clearTrack(link: Link): void {
do {
const dep = link.dep
const nextDep = link.nextDep
const nextSub = link.nextSub
const prevSub = link.prevSub
if (nextSub !== undefined) {
nextSub.prevSub = prevSub
link.nextSub = undefined
} else {
dep.subsTail = prevSub
if ('lastTrackedId' in dep) {
dep.lastTrackedId = 0
}
}
if (prevSub !== undefined) {
prevSub.nextSub = nextSub
link.prevSub = undefined
} else {
dep.subs = nextSub
}
// @ts-expect-error
link.dep = undefined
// @ts-expect-error
link.sub = undefined
link.nextDep = linkPool
linkPool = link
if (dep.subs === undefined && 'deps' in dep) {
if ('notify' in dep) {
dep.flags &= ~SubscriberFlags.Dirtys
} else {
dep.flags |= SubscriberFlags.Dirty
}
const depDeps = dep.deps
if (depDeps !== undefined) {
link = depDeps
dep.depsTail!.nextDep = nextDep
dep.deps = undefined
dep.depsTail = undefined
continue
}
}
link = nextDep!
} while (link !== undefined)
}

View File

@ -10,20 +10,19 @@ import {
isSet,
remove,
} from '@vue/shared'
import { warn } from './warning'
import type { ComputedRef } from './computed'
import { ReactiveFlags } from './constants'
import {
type DebuggerOptions,
EffectFlags,
type EffectScheduler,
ReactiveEffect,
pauseTracking,
resetTracking,
} from './effect'
import { getCurrentScope } from './effectScope'
import { isReactive, isShallow } from './reactive'
import { type Ref, isRef } from './ref'
import { getCurrentScope } from './effectScope'
import { warn } from './warning'
// These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
// to @vue/reactivity to allow co-location with the moved base watch logic, hence
@ -231,10 +230,7 @@ export function watch(
: INITIAL_WATCHER_VALUE
const job = (immediateFirstRun?: boolean) => {
if (
!(effect.flags & EffectFlags.ACTIVE) ||
(!effect.dirty && !immediateFirstRun)
) {
if (!effect.active || (!immediateFirstRun && !effect.dirty)) {
return
}
if (cb) {

View File

@ -1,3 +1,8 @@
import {
type ComputedRefImpl,
type ReactiveEffectRunner,
effect,
} from '@vue/reactivity'
import {
type ComponentInternalInstance,
type SetupContext,
@ -25,8 +30,6 @@ import {
withAsyncContext,
withDefaults,
} from '../src/apiSetupHelpers'
import type { ComputedRefImpl } from '../../reactivity/src/computed'
import { EffectFlags, type ReactiveEffectRunner, effect } from '@vue/reactivity'
describe('SFC <script setup> helpers', () => {
test('should warn runtime usage', () => {
@ -450,12 +453,12 @@ describe('SFC <script setup> helpers', () => {
app.mount(root)
await ready
expect(e!.effect.flags & EffectFlags.ACTIVE).toBeTruthy()
expect(c!.flags & EffectFlags.TRACKING).toBeTruthy()
expect(e!.effect.active).toBeTruthy()
expect(c!.flags & 1 /* SubscriberFlags.Tracking */).toBe(0)
app.unmount()
expect(e!.effect.flags & EffectFlags.ACTIVE).toBeFalsy()
expect(c!.flags & EffectFlags.TRACKING).toBeFalsy()
expect(e!.effect.active).toBeFalsy()
expect(c!.flags & 1 /* SubscriberFlags.Tracking */).toBe(0)
})
})
})

View File

@ -531,6 +531,9 @@ describe('error handling', () => {
caughtError = caught
}
expect(fn).toHaveBeenCalledWith(err, 'setup function')
expect(
`Active effect was not restored correctly - this is likely a Vue internal bug.`,
).toHaveBeenWarned()
expect(
`Unhandled error during execution of setup function`,
).toHaveBeenWarned()

View File

@ -1558,7 +1558,8 @@ function baseCreateRenderer(
instance.scope.off()
const update = (instance.update = effect.run.bind(effect))
const job: SchedulerJob = (instance.job = effect.runIfDirty.bind(effect))
const job: SchedulerJob = (instance.job = () =>
effect.dirty && effect.run())
job.i = instance
job.id = instance.uid
effect.scheduler = () => queueJob(job)

View File

@ -3,7 +3,7 @@ import { entries } from './scripts/aliases.js'
export default defineConfig({
define: {
__DEV__: true,
__DEV__: process.env.MODE !== 'benchmark',
__TEST__: true,
__VERSION__: '"test"',
__BROWSER__: false,
@ -24,6 +24,11 @@ export default defineConfig({
test: {
globals: true,
pool: 'threads',
poolOptions: {
forks: {
execArgv: ['--expose-gc'],
},
},
setupFiles: 'scripts/setup-vitest.ts',
environmentMatchGlobs: [
['packages/{vue,vue-compat,runtime-dom}/**', 'jsdom'],