From 2e5dd06fb6dd20edf5d6b741b5a6f6af4b5cbaaa Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 21 Feb 2024 17:38:16 +0800 Subject: [PATCH 01/43] wip: save --- .../reactivity/__tests__/computed.spec.ts | 2 +- packages/reactivity/__tests__/effect.spec.ts | 2 +- .../reactivity/__tests__/reactive.spec.ts | 6 +- .../__tests__/reactiveArray.spec.ts | 4 +- packages/reactivity/__tests__/ref.spec.ts | 2 +- .../__tests__/shallowReactive.spec.ts | 4 +- packages/reactivity/src/baseHandlers.ts | 4 +- packages/reactivity/src/computed-old.ts | 168 +++++ packages/reactivity/src/computed.ts | 175 ++---- packages/reactivity/src/deferredComputed.ts | 2 +- packages/reactivity/src/dep.ts | 4 +- packages/reactivity/src/effect-old.ts | 327 ++++++++++ packages/reactivity/src/effect.ts | 588 ++++++++++-------- packages/reactivity/src/effectScope.ts | 2 +- packages/reactivity/src/index.ts | 6 +- packages/reactivity/src/reactive.ts | 2 +- packages/reactivity/src/reactiveEffect.ts | 2 +- packages/reactivity/src/ref-old.ts | 532 ++++++++++++++++ packages/reactivity/src/ref.ts | 83 +-- 19 files changed, 1432 insertions(+), 483 deletions(-) create mode 100644 packages/reactivity/src/computed-old.ts create mode 100644 packages/reactivity/src/effect-old.ts create mode 100644 packages/reactivity/src/ref-old.ts diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c9f47720edd..0e8038d24b1 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -14,7 +14,7 @@ import { toRaw, } from '../src' import { DirtyLevels } from '../src/constants' -import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed' +import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed-old' describe('reactivity/computed', () => { it('should return updated value', () => { diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index bd26934f1ce..e0ab729b229 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -11,7 +11,7 @@ import { stop, toRaw, } from '../src/index' -import { pauseScheduling, resetScheduling } from '../src/effect' +import { pauseScheduling, resetScheduling } from '../src/effect-old' import { ITERATE_KEY, getDepFromReactive } from '../src/reactiveEffect' import { computed, diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index ab953ff891a..a9f42586220 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -1,7 +1,7 @@ -import { isRef, ref } from '../src/ref' +import { isRef, ref } from '../src/ref-old' import { isReactive, markRaw, reactive, toRaw } from '../src/reactive' -import { computed } from '../src/computed' -import { effect } from '../src/effect' +import { computed } from '../src/computed-old' +import { effect } from '../src/effect-old' describe('reactivity/reactive', () => { test('Object', () => { diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts index 1c6fcefd592..72108efa1a4 100644 --- a/packages/reactivity/__tests__/reactiveArray.spec.ts +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -1,6 +1,6 @@ import { isReactive, reactive, toRaw } from '../src/reactive' -import { isRef, ref } from '../src/ref' -import { effect } from '../src/effect' +import { isRef, ref } from '../src/ref-old' +import { effect } from '../src/effect-old' describe('reactivity/reactive/Array', () => { test('should make Array reactive', () => { diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 2b2024d9723..76a326cc0a3 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -9,7 +9,7 @@ import { toRefs, } from '../src/index' import { computed } from '@vue/runtime-dom' -import { customRef, shallowRef, triggerRef, unref } from '../src/ref' +import { customRef, shallowRef, triggerRef, unref } from '../src/ref-old' import { isReadonly, isShallow, diff --git a/packages/reactivity/__tests__/shallowReactive.spec.ts b/packages/reactivity/__tests__/shallowReactive.spec.ts index e9b64d39b36..00fc5026685 100644 --- a/packages/reactivity/__tests__/shallowReactive.spec.ts +++ b/packages/reactivity/__tests__/shallowReactive.spec.ts @@ -6,8 +6,8 @@ import { shallowReadonly, } from '../src/reactive' -import { effect } from '../src/effect' -import { type Ref, isRef, ref } from '../src/ref' +import { effect } from '../src/effect-old' +import { type Ref, isRef, ref } from '../src/ref-old' describe('shallowReactive', () => { test('should not make non-reactive properties reactive', () => { diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index a1b3003a5e7..3282a020077 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -16,7 +16,7 @@ import { pauseTracking, resetScheduling, resetTracking, -} from './effect' +} from './effect-old' import { ITERATE_KEY, track, trigger } from './reactiveEffect' import { hasChanged, @@ -27,7 +27,7 @@ import { isSymbol, makeMap, } from '@vue/shared' -import { isRef } from './ref' +import { isRef } from './ref-old' import { warn } from './warning' const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`) diff --git a/packages/reactivity/src/computed-old.ts b/packages/reactivity/src/computed-old.ts new file mode 100644 index 00000000000..da92dd155b1 --- /dev/null +++ b/packages/reactivity/src/computed-old.ts @@ -0,0 +1,168 @@ +import { type DebuggerOptions, ReactiveEffect } from './effect-old' +import { type Ref, trackRefValue, triggerRefValue } from './ref-old' +import { NOOP, hasChanged, isFunction } from '@vue/shared' +import { toRaw } from './reactive' +import type { Dep } from './dep' +import { DirtyLevels, ReactiveFlags } from './constants' +import { warn } from './warning' + +declare const ComputedRefSymbol: unique symbol + +export interface ComputedRef extends WritableComputedRef { + readonly value: T + [ComputedRefSymbol]: true +} + +export interface WritableComputedRef extends Ref { + readonly effect: ReactiveEffect +} + +export type ComputedGetter = (oldValue?: T) => T +export type ComputedSetter = (newValue: T) => void + +export interface WritableComputedOptions { + get: ComputedGetter + set: ComputedSetter +} + +export const COMPUTED_SIDE_EFFECT_WARN = + `Computed is still dirty after getter evaluation,` + + ` likely because a computed is mutating its own dependency in its getter.` + + ` State mutations in computed getters should be avoided. ` + + ` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free` + +export class ComputedRefImpl { + public dep?: Dep = undefined + + private _value!: T + public readonly effect: ReactiveEffect + + public readonly __v_isRef = true + public readonly [ReactiveFlags.IS_READONLY]: boolean = false + + public _cacheable: boolean + + constructor( + getter: ComputedGetter, + private readonly _setter: ComputedSetter, + isReadonly: boolean, + isSSR: boolean, + ) { + this.effect = new ReactiveEffect( + () => getter(this._value), + () => + triggerRefValue( + this, + this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect + ? DirtyLevels.MaybeDirty_ComputedSideEffect + : DirtyLevels.MaybeDirty, + ), + ) + this.effect.computed = this + this.effect.active = this._cacheable = !isSSR + this[ReactiveFlags.IS_READONLY] = isReadonly + } + + get value() { + // the computed ref may get wrapped by other proxies e.g. readonly() #3376 + const self = toRaw(this) + if ( + (!self._cacheable || self.effect.dirty) && + hasChanged(self._value, (self._value = self.effect.run()!)) + ) { + triggerRefValue(self, DirtyLevels.Dirty) + } + trackRefValue(self) + if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { + __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN) + triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) + } + return self._value + } + + set value(newValue: T) { + this._setter(newValue) + } + + // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x + get _dirty() { + return this.effect.dirty + } + + set _dirty(v) { + this.effect.dirty = v + } + // #endregion +} + +/** + * Takes a getter function and returns a readonly reactive ref object for the + * returned value from the getter. It can also take an object with get and set + * functions to create a writable ref object. + * + * @example + * ```js + * // Creating a readonly computed ref: + * const count = ref(1) + * const plusOne = computed(() => count.value + 1) + * + * console.log(plusOne.value) // 2 + * plusOne.value++ // error + * ``` + * + * ```js + * // Creating a writable computed ref: + * const count = ref(1) + * const plusOne = computed({ + * get: () => count.value + 1, + * set: (val) => { + * count.value = val - 1 + * } + * }) + * + * plusOne.value = 1 + * console.log(count.value) // 0 + * ``` + * + * @param getter - Function that produces the next value. + * @param debugOptions - For debugging. See {@link https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging}. + * @see {@link https://vuejs.org/api/reactivity-core.html#computed} + */ +export function computed( + getter: ComputedGetter, + debugOptions?: DebuggerOptions, +): ComputedRef +export function computed( + options: WritableComputedOptions, + debugOptions?: DebuggerOptions, +): WritableComputedRef +export function computed( + getterOrOptions: ComputedGetter | WritableComputedOptions, + debugOptions?: DebuggerOptions, + isSSR = false, +) { + let getter: ComputedGetter + let setter: ComputedSetter + + const onlyGetter = isFunction(getterOrOptions) + if (onlyGetter) { + getter = getterOrOptions + setter = __DEV__ + ? () => { + warn('Write operation failed: computed value is readonly') + } + : NOOP + } else { + getter = getterOrOptions.get + setter = getterOrOptions.set + } + + const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR) + + if (__DEV__ && debugOptions && !isSSR) { + cRef.effect.onTrack = debugOptions.onTrack + cRef.effect.onTrigger = debugOptions.onTrigger + } + + return cRef as any +} diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index a4b74172fcf..2fcc351d77c 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,10 +1,12 @@ -import { type DebuggerOptions, ReactiveEffect } from './effect' -import { type Ref, trackRefValue, triggerRefValue } from './ref' -import { NOOP, hasChanged, isFunction } from '@vue/shared' -import { toRaw } from './reactive' -import type { Dep } from './dep' -import { DirtyLevels, ReactiveFlags } from './constants' -import { warn } from './warning' +import { + Dep, + Flags, + type Link, + type ReactiveEffect, + type Subscriber, + refreshComputed, +} from './effect' +import type { Ref } from './ref' declare const ComputedRefSymbol: unique symbol @@ -14,7 +16,7 @@ export interface ComputedRef extends WritableComputedRef { } export interface WritableComputedRef extends Ref { - readonly effect: ReactiveEffect + readonly effect: ReactiveEffect } export type ComputedGetter = (oldValue?: T) => T @@ -25,144 +27,37 @@ export interface WritableComputedOptions { set: ComputedSetter } -export const COMPUTED_SIDE_EFFECT_WARN = - `Computed is still dirty after getter evaluation,` + - ` likely because a computed is mutating its own dependency in its getter.` + - ` State mutations in computed getters should be avoided. ` + - ` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free` - -export class ComputedRefImpl { - public dep?: Dep = undefined - - private _value!: T - public readonly effect: ReactiveEffect - - public readonly __v_isRef = true - public readonly [ReactiveFlags.IS_READONLY]: boolean = false - - public _cacheable: boolean - - constructor( - getter: ComputedGetter, - private readonly _setter: ComputedSetter, - isReadonly: boolean, - isSSR: boolean, - ) { - this.effect = new ReactiveEffect( - () => getter(this._value), - () => - triggerRefValue( - this, - this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect - ? DirtyLevels.MaybeDirty_ComputedSideEffect - : DirtyLevels.MaybeDirty, - ), - ) - this.effect.computed = this - this.effect.active = this._cacheable = !isSSR - this[ReactiveFlags.IS_READONLY] = isReadonly +export class ComputedRefImpl implements Subscriber { + // A computed is a ref + _value: any = undefined + dep: Dep + // A computed is also a subscriber that tracks other deps + deps?: Link = undefined + // track variaous states + flags = Flags.DIRTY + + constructor(public getter: ComputedGetter) { + this.dep = new Dep(this) } - get value() { - // the computed ref may get wrapped by other proxies e.g. readonly() #3376 - const self = toRaw(this) - if ( - (!self._cacheable || self.effect.dirty) && - hasChanged(self._value, (self._value = self.effect.run()!)) - ) { - triggerRefValue(self, DirtyLevels.Dirty) - } - trackRefValue(self) - if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { - __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN) - triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) + notify() { + if (!(this.flags & Flags.NOTIFIED)) { + this.flags |= Flags.DIRTY | Flags.NOTIFIED + this.dep.notify() } - return self._value } - set value(newValue: T) { - this._setter(newValue) - } - - // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x - get _dirty() { - return this.effect.dirty - } - - set _dirty(v) { - this.effect.dirty = v + get value() { + const link = this.dep.track() + refreshComputed(this) + // sync version after evaluation + if (link) { + link.version = this.dep.version + } + return this._value } - // #endregion } -/** - * Takes a getter function and returns a readonly reactive ref object for the - * returned value from the getter. It can also take an object with get and set - * functions to create a writable ref object. - * - * @example - * ```js - * // Creating a readonly computed ref: - * const count = ref(1) - * const plusOne = computed(() => count.value + 1) - * - * console.log(plusOne.value) // 2 - * plusOne.value++ // error - * ``` - * - * ```js - * // Creating a writable computed ref: - * const count = ref(1) - * const plusOne = computed({ - * get: () => count.value + 1, - * set: (val) => { - * count.value = val - 1 - * } - * }) - * - * plusOne.value = 1 - * console.log(count.value) // 0 - * ``` - * - * @param getter - Function that produces the next value. - * @param debugOptions - For debugging. See {@link https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging}. - * @see {@link https://vuejs.org/api/reactivity-core.html#computed} - */ -export function computed( - getter: ComputedGetter, - debugOptions?: DebuggerOptions, -): ComputedRef -export function computed( - options: WritableComputedOptions, - debugOptions?: DebuggerOptions, -): WritableComputedRef -export function computed( - getterOrOptions: ComputedGetter | WritableComputedOptions, - debugOptions?: DebuggerOptions, - isSSR = false, -) { - let getter: ComputedGetter - let setter: ComputedSetter - - const onlyGetter = isFunction(getterOrOptions) - if (onlyGetter) { - getter = getterOrOptions - setter = __DEV__ - ? () => { - warn('Write operation failed: computed value is readonly') - } - : NOOP - } else { - getter = getterOrOptions.get - setter = getterOrOptions.set - } - - const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR) - - if (__DEV__ && debugOptions && !isSSR) { - cRef.effect.onTrack = debugOptions.onTrack - cRef.effect.onTrigger = debugOptions.onTrigger - } - - return cRef as any +export function computed(getter: ComputedGetter) { + return new ComputedRefImpl(getter) } diff --git a/packages/reactivity/src/deferredComputed.ts b/packages/reactivity/src/deferredComputed.ts index 1dbba1f3f03..d384216ae66 100644 --- a/packages/reactivity/src/deferredComputed.ts +++ b/packages/reactivity/src/deferredComputed.ts @@ -1,4 +1,4 @@ -import { computed } from './computed' +import { computed } from './computed-old' /** * @deprecated use `computed` instead. See #5912 diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index c8e8a130dc9..52a44352790 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -1,5 +1,5 @@ -import type { ReactiveEffect } from './effect' -import type { ComputedRefImpl } from './computed' +import type { ReactiveEffect } from './effect-old' +import type { ComputedRefImpl } from './computed-old' export type Dep = Map & { cleanup: () => void diff --git a/packages/reactivity/src/effect-old.ts b/packages/reactivity/src/effect-old.ts new file mode 100644 index 00000000000..1375c463da6 --- /dev/null +++ b/packages/reactivity/src/effect-old.ts @@ -0,0 +1,327 @@ +import { NOOP, extend } from '@vue/shared' +import type { ComputedRefImpl } from './computed-old' +import { + DirtyLevels, + type TrackOpTypes, + type TriggerOpTypes, +} from './constants' +import type { Dep } from './dep' +import { type EffectScope, recordEffectScope } from './effectScope' + +export type EffectScheduler = (...args: any[]) => any + +export type DebuggerEvent = { + effect: ReactiveEffect +} & DebuggerEventExtraInfo + +export type DebuggerEventExtraInfo = { + target: object + type: TrackOpTypes | TriggerOpTypes + key: any + newValue?: any + oldValue?: any + oldTarget?: Map | Set +} + +export let activeEffect: ReactiveEffect | undefined + +export class ReactiveEffect { + active = true + deps: Dep[] = [] + + /** + * Can be attached after creation + * @internal + */ + computed?: ComputedRefImpl + /** + * @internal + */ + allowRecurse?: boolean + + onStop?: () => void + // dev only + onTrack?: (event: DebuggerEvent) => void + // dev only + onTrigger?: (event: DebuggerEvent) => void + + /** + * @internal + */ + _dirtyLevel = DirtyLevels.Dirty + /** + * @internal + */ + _trackId = 0 + /** + * @internal + */ + _runnings = 0 + /** + * @internal + */ + _shouldSchedule = false + /** + * @internal + */ + _depsLength = 0 + + constructor( + public fn: () => T, + public trigger: () => void, + public scheduler?: EffectScheduler, + scope?: EffectScope, + ) { + recordEffectScope(this, scope) + } + + public get dirty() { + if ( + this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect || + this._dirtyLevel === DirtyLevels.MaybeDirty + ) { + this._dirtyLevel = DirtyLevels.QueryingDirty + pauseTracking() + for (let i = 0; i < this._depsLength; i++) { + const dep = this.deps[i] + if (dep.computed) { + triggerComputed(dep.computed) + if (this._dirtyLevel >= DirtyLevels.Dirty) { + break + } + } + } + if (this._dirtyLevel === DirtyLevels.QueryingDirty) { + this._dirtyLevel = DirtyLevels.NotDirty + } + resetTracking() + } + return this._dirtyLevel >= DirtyLevels.Dirty + } + + public set dirty(v) { + this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty + } + + run() { + this._dirtyLevel = DirtyLevels.NotDirty + if (!this.active) { + return this.fn() + } + let lastShouldTrack = shouldTrack + let lastEffect = activeEffect + try { + shouldTrack = true + activeEffect = this + this._runnings++ + preCleanupEffect(this) + return this.fn() + } finally { + postCleanupEffect(this) + this._runnings-- + activeEffect = lastEffect + shouldTrack = lastShouldTrack + } + } + + stop() { + if (this.active) { + preCleanupEffect(this) + postCleanupEffect(this) + this.onStop?.() + this.active = false + } + } +} + +function triggerComputed(computed: ComputedRefImpl) { + return computed.value +} + +function preCleanupEffect(effect: ReactiveEffect) { + effect._trackId++ + effect._depsLength = 0 +} + +function postCleanupEffect(effect: ReactiveEffect) { + if (effect.deps.length > effect._depsLength) { + for (let i = effect._depsLength; i < effect.deps.length; i++) { + cleanupDepEffect(effect.deps[i], effect) + } + effect.deps.length = effect._depsLength + } +} + +function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) { + const trackId = dep.get(effect) + if (trackId !== undefined && effect._trackId !== trackId) { + dep.delete(effect) + if (dep.size === 0) { + dep.cleanup() + } + } +} + +export interface DebuggerOptions { + onTrack?: (event: DebuggerEvent) => void + onTrigger?: (event: DebuggerEvent) => void +} + +export interface ReactiveEffectOptions extends DebuggerOptions { + lazy?: boolean + scheduler?: EffectScheduler + scope?: EffectScope + allowRecurse?: boolean + onStop?: () => void +} + +export interface ReactiveEffectRunner { + (): T + effect: ReactiveEffect +} + +/** + * Registers the given function to track reactive updates. + * + * The given function will be run once immediately. Every time any reactive + * property that's accessed within it gets updated, the function will run again. + * + * @param fn - The function that will track reactive updates. + * @param options - Allows to control the effect's behaviour. + * @returns A runner that can be used to control the effect after creation. + */ +export function effect( + fn: () => T, + options?: ReactiveEffectOptions, +): ReactiveEffectRunner { + if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) { + fn = (fn as ReactiveEffectRunner).effect.fn + } + + const _effect = new ReactiveEffect(fn, NOOP, () => { + if (_effect.dirty) { + _effect.run() + } + }) + if (options) { + extend(_effect, options) + if (options.scope) recordEffectScope(_effect, options.scope) + } + if (!options || !options.lazy) { + _effect.run() + } + const runner = _effect.run.bind(_effect) as ReactiveEffectRunner + runner.effect = _effect + return runner +} + +/** + * Stops the effect associated with the given runner. + * + * @param runner - Association with the effect to stop tracking. + */ +export function stop(runner: ReactiveEffectRunner) { + runner.effect.stop() +} + +export let shouldTrack = true +export let pauseScheduleStack = 0 + +const trackStack: boolean[] = [] + +/** + * Temporarily pauses tracking. + */ +export function pauseTracking() { + trackStack.push(shouldTrack) + shouldTrack = false +} + +/** + * Re-enables effect tracking (if it was paused). + */ +export function enableTracking() { + trackStack.push(shouldTrack) + shouldTrack = true +} + +/** + * Resets the previous global effect tracking state. + */ +export function resetTracking() { + const last = trackStack.pop() + shouldTrack = last === undefined ? true : last +} + +export function pauseScheduling() { + pauseScheduleStack++ +} + +export function resetScheduling() { + pauseScheduleStack-- + while (!pauseScheduleStack && queueEffectSchedulers.length) { + queueEffectSchedulers.shift()!() + } +} + +export function trackEffect( + effect: ReactiveEffect, + dep: Dep, + debuggerEventExtraInfo?: DebuggerEventExtraInfo, +) { + if (dep.get(effect) !== effect._trackId) { + dep.set(effect, effect._trackId) + const oldDep = effect.deps[effect._depsLength] + if (oldDep !== dep) { + if (oldDep) { + cleanupDepEffect(oldDep, effect) + } + effect.deps[effect._depsLength++] = dep + } else { + effect._depsLength++ + } + if (__DEV__) { + effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!)) + } + } +} + +const queueEffectSchedulers: EffectScheduler[] = [] + +export function triggerEffects( + dep: Dep, + dirtyLevel: DirtyLevels, + debuggerEventExtraInfo?: DebuggerEventExtraInfo, +) { + pauseScheduling() + for (const effect of dep.keys()) { + // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result + let tracking: boolean | undefined + if ( + effect._dirtyLevel < dirtyLevel && + (tracking ??= dep.get(effect) === effect._trackId) + ) { + effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty + effect._dirtyLevel = dirtyLevel + } + if ( + effect._shouldSchedule && + (tracking ??= dep.get(effect) === effect._trackId) + ) { + if (__DEV__) { + effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo)) + } + effect.trigger() + if ( + (!effect._runnings || effect.allowRecurse) && + effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect + ) { + effect._shouldSchedule = false + if (effect.scheduler) { + queueEffectSchedulers.push(effect.scheduler) + } + } + } + } + resetScheduling() +} diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index ca90544c0de..f8dae5c112c 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,327 +1,411 @@ -import { NOOP, extend } from '@vue/shared' import type { ComputedRefImpl } from './computed' -import { - DirtyLevels, - type TrackOpTypes, - type TriggerOpTypes, -} from './constants' -import type { Dep } from './dep' -import { type EffectScope, recordEffectScope } from './effectScope' -export type EffectScheduler = (...args: any[]) => any +export let activeSub: Subscriber | undefined -export type DebuggerEvent = { - effect: ReactiveEffect -} & DebuggerEventExtraInfo - -export type DebuggerEventExtraInfo = { - target: object - type: TrackOpTypes | TriggerOpTypes - key: any - newValue?: any - oldValue?: any - oldTarget?: Map | Set +export enum Flags { + ACTIVE = 1 << 0, + RUNNING = 1 << 1, + TRACKING = 1 << 2, + NOTIFIED = 1 << 3, + DIRTY = 1 << 4, + HAS_ERROR = 1 << 5, } -export let activeEffect: ReactiveEffect | undefined - -export class ReactiveEffect { - active = true - deps: Dep[] = [] - - /** - * Can be attached after creation - * @internal - */ - computed?: ComputedRefImpl +/** + * Subscriber is a type that tracks (or subscribes to) a list of deps. + */ +export interface Subscriber { /** - * @internal + * doubly linked list representing the deps + * set to tail before effect run + * points to tail during effect run + * set to head after effect run */ - allowRecurse?: boolean + deps?: Link + flags: Flags + notify(): void +} - onStop?: () => void - // dev only - onTrack?: (event: DebuggerEvent) => void - // dev only - onTrigger?: (event: DebuggerEvent) => void +/** + * 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. + */ +export interface Link { + dep: Dep + sub: Subscriber /** - * @internal + * - 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 */ - _dirtyLevel = DirtyLevels.Dirty - /** - * @internal - */ - _trackId = 0 + version: number + /** - * @internal + * Pointers for doubly-linked lists */ - _runnings = 0 + nextDep?: Link + prevDep?: Link + + nextSub?: Link + prevSub?: Link + + prevActiveLink?: Link +} + +export class Dep { + version = 0 /** - * @internal + * Link between this dep and the current active effect */ - _shouldSchedule = false + activeLink?: Link = undefined /** - * @internal + * Doubly linked list representing the subscribing effects (tail) */ - _depsLength = 0 - - constructor( - public fn: () => T, - public trigger: () => void, - public scheduler?: EffectScheduler, - scope?: EffectScope, - ) { - recordEffectScope(this, scope) - } + subs?: Link = undefined - public get dirty() { - if ( - this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect || - this._dirtyLevel === DirtyLevels.MaybeDirty - ) { - this._dirtyLevel = DirtyLevels.QueryingDirty - pauseTracking() - for (let i = 0; i < this._depsLength; i++) { - const dep = this.deps[i] - if (dep.computed) { - triggerComputed(dep.computed) - if (this._dirtyLevel >= DirtyLevels.Dirty) { - break + constructor(public computed?: ComputedRefImpl) {} + + track(): Link | undefined { + if (activeSub === undefined) { + return + } + + let link = this.activeLink + if (link === undefined || link.sub !== activeSub) { + link = this.activeLink = { + dep: this, + sub: activeSub, + version: this.version, + nextDep: undefined, + prevDep: activeSub.deps, + nextSub: undefined, + prevSub: undefined, + prevActiveLink: undefined, + } + + // add the link to the activeEffect as a dep (as tail) + if (activeSub.deps) { + activeSub.deps.nextDep = link + } + activeSub.deps = link + + // add the link to this dep as a subscriber (as tail) + if (activeSub.flags & Flags.TRACKING) { + const computed = this.computed + if (computed && !this.subs) { + // a computed dep getting its first subscriber, enable tracking + + // lazily subscribe to all its deps + computed.flags |= Flags.TRACKING | Flags.DIRTY + for (let l = computed.deps; l !== undefined; l = l.nextDep) { + l.dep.addSub(l) } } + this.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) { + link.nextDep.prevDep = link.prevDep + if (link.prevDep) { + link.prevDep.nextDep = link.nextDep + } + + link.prevDep = activeSub.deps + link.nextDep = undefined + + activeSub.deps!.nextDep = link + activeSub.deps = link } - if (this._dirtyLevel === DirtyLevels.QueryingDirty) { - this._dirtyLevel = DirtyLevels.NotDirty + } + return link + } + + addSub(link: Link) { + if (this.subs !== link) { + link.prevSub = this.subs + if (this.subs) { + this.subs.nextSub = link } - resetTracking() + this.subs = link } - return this._dirtyLevel >= DirtyLevels.Dirty } - public set dirty(v) { - this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty + trigger() { + this.version++ + this.notify() + } + + notify() { + batchDepth++ + try { + for (let link = this.subs; link !== undefined; link = link.prevSub) { + link.sub.notify() + } + } finally { + runBatchedEffects() + } + } +} + +export type EffectScheduler = (run: () => any) => any + +export class ReactiveEffect implements Subscriber { + deps?: Link = undefined + flags: Flags = Flags.ACTIVE | Flags.TRACKING + + scheduler?: EffectScheduler = undefined + nextEffect?: ReactiveEffect = undefined + + constructor(private _fn: () => any) {} + + notify() { + if (!(this.flags & Flags.NOTIFIED)) { + this.flags |= Flags.NOTIFIED + this.nextEffect = batchedEffect + batchedEffect = this + } } run() { - this._dirtyLevel = DirtyLevels.NotDirty - if (!this.active) { - return this.fn() + this.flags |= Flags.RUNNING + + // TODO cleanupEffect + + if (!(this.flags & Flags.ACTIVE)) { + // stopped during cleanup + return } - let lastShouldTrack = shouldTrack - let lastEffect = activeEffect + + prepareDeps(this) + batchDepth++ + const prevEffect = activeSub + activeSub = this + try { - shouldTrack = true - activeEffect = this - this._runnings++ - preCleanupEffect(this) - return this.fn() + this._fn() } finally { - postCleanupEffect(this) - this._runnings-- - activeEffect = lastEffect - shouldTrack = lastShouldTrack + // TODO make this dev only + if (activeSub !== this) { + throw new Error('active effect was not restored correctly') + } + cleanupDeps(this) + activeSub = prevEffect + this.flags &= ~Flags.RUNNING + runBatchedEffects() } } stop() { - if (this.active) { - preCleanupEffect(this) - postCleanupEffect(this) - this.onStop?.() - this.active = false + if (this.flags & Flags.ACTIVE) { + this.flags &= ~Flags.ACTIVE + for (let link = this.deps; link !== undefined; link = link.nextDep) { + removeSub(link) + } + this.deps = undefined } } } -function triggerComputed(computed: ComputedRefImpl) { - return computed.value -} +let batchDepth = 0 +let batchedEffect: ReactiveEffect | undefined -function preCleanupEffect(effect: ReactiveEffect) { - effect._trackId++ - effect._depsLength = 0 -} +function runBatchedEffects() { + if (batchDepth > 1) { + batchDepth-- + return + } -function postCleanupEffect(effect: ReactiveEffect) { - if (effect.deps.length > effect._depsLength) { - for (let i = effect._depsLength; i < effect.deps.length; i++) { - cleanupDepEffect(effect.deps[i], effect) + let error: unknown + while (batchedEffect) { + let e: ReactiveEffect | undefined = batchedEffect + batchedEffect = undefined + while (e) { + const next: ReactiveEffect | undefined = e.nextEffect + e.nextEffect = undefined + e.flags &= ~Flags.NOTIFIED + if (e.flags & Flags.ACTIVE && isDirty(e)) { + try { + if (e.scheduler) { + e.scheduler(e.run.bind(e)) + } else { + e.run() + } + } catch (err) { + if (!error) error = err + } + } + e = next } - effect.deps.length = effect._depsLength } + + batchDepth-- + if (error) throw error } -function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) { - const trackId = dep.get(effect) - if (trackId !== undefined && effect._trackId !== trackId) { - dep.delete(effect) - if (dep.size === 0) { - dep.cleanup() +function prepareDeps(sub: Subscriber) { + // Prepare deps for tracking, starting from the head + for (let link = sub.deps; link !== undefined; 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 + if (!link.nextDep) { + // point deps to the tail + sub.deps = link + break } } } -export interface DebuggerOptions { - onTrack?: (event: DebuggerEvent) => void - onTrigger?: (event: DebuggerEvent) => void -} +function cleanupDeps(sub: Subscriber) { + // Cleanup unsued deps, starting from the tail + let link = sub.deps + let head + while (link) { + if (link.version === -1) { + // 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 + } -export interface ReactiveEffectOptions extends DebuggerOptions { - lazy?: boolean - scheduler?: EffectScheduler - scope?: EffectScope - allowRecurse?: boolean - onStop?: () => void -} + // restore previous active link if any + link.dep.activeLink = link.prevActiveLink + link.prevActiveLink = undefined -export interface ReactiveEffectRunner { - (): T - effect: ReactiveEffect -} - -/** - * Registers the given function to track reactive updates. - * - * The given function will be run once immediately. Every time any reactive - * property that's accessed within it gets updated, the function will run again. - * - * @param fn - The function that will track reactive updates. - * @param options - Allows to control the effect's behaviour. - * @returns A runner that can be used to control the effect after creation. - */ -export function effect( - fn: () => T, - options?: ReactiveEffectOptions, -): ReactiveEffectRunner { - if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) { - fn = (fn as ReactiveEffectRunner).effect.fn + link = link.prevDep } + // set the new head + sub.deps = head +} - const _effect = new ReactiveEffect(fn, NOOP, () => { - if (_effect.dirty) { - _effect.run() +function isDirty(sub: Subscriber): boolean { + for (let link = sub.deps; link !== undefined; link = link.nextDep) { + if ( + link.dep.version !== link.version || + (link.dep.computed && refreshComputed(link.dep.computed) === false) || + link.dep.version !== link.version + ) { + return true } - }) - if (options) { - extend(_effect, options) - if (options.scope) recordEffectScope(_effect, options.scope) } - if (!options || !options.lazy) { - _effect.run() - } - const runner = _effect.run.bind(_effect) as ReactiveEffectRunner - runner.effect = _effect - return runner + return false } /** - * Stops the effect associated with the given runner. - * - * @param runner - Association with the effect to stop tracking. + * Returning false indicates the refresh failed */ -export function stop(runner: ReactiveEffectRunner) { - runner.effect.stop() -} +export function refreshComputed(computed: ComputedRefImpl) { + computed.flags &= ~Flags.NOTIFIED -export let shouldTrack = true -export let pauseScheduleStack = 0 + if (computed.flags & Flags.RUNNING) { + return false + } + if (computed.flags & Flags.TRACKING && !(computed.flags & Flags.DIRTY)) { + return + } + computed.flags &= ~Flags.DIRTY -const trackStack: boolean[] = [] + // TODO global version fast path -/** - * Temporarily pauses tracking. - */ -export function pauseTracking() { - trackStack.push(shouldTrack) - shouldTrack = false -} + const dep = computed.dep + computed.flags |= Flags.RUNNING + if (dep.version > 0 && !isDirty(computed)) { + computed.flags &= ~Flags.RUNNING + return + } -/** - * Re-enables effect tracking (if it was paused). - */ -export function enableTracking() { - trackStack.push(shouldTrack) - shouldTrack = true -} + const prevSub = activeSub + activeSub = computed + try { + prepareDeps(computed) + const value = computed.getter() + if (dep.version === 0 || !Object.is(value, computed._value)) { + computed._value = value + dep.version++ + } + } catch (err) { + // TODO error recovery? + dep.version++ + } -/** - * Resets the previous global effect tracking state. - */ -export function resetTracking() { - const last = trackStack.pop() - shouldTrack = last === undefined ? true : last + activeSub = prevSub + cleanupDeps(computed) + computed.flags &= ~Flags.RUNNING } -export function pauseScheduling() { - pauseScheduleStack++ -} +function removeSub(link: Link) { + const prevEffect = link.prevSub + const nextEffect = link.nextSub + if (prevEffect) { + prevEffect.nextSub = nextEffect + link.prevSub = undefined + } + if (nextEffect) { + nextEffect.prevSub = prevEffect + link.nextSub = undefined + } + if (link.dep.subs === link) { + // was previous tail, point new tail to prev + link.dep.subs = prevEffect + } -export function resetScheduling() { - pauseScheduleStack-- - while (!pauseScheduleStack && queueEffectSchedulers.length) { - queueEffectSchedulers.shift()!() + // computed dep has lost its last subscriber + // turn off tracking flag and also unsubscribe it from all its deps + const computed = link.dep.computed + if (computed && link.dep.subs === undefined) { + computed.flags &= ~Flags.TRACKING + for (let l = computed.deps; l !== undefined; l = l.nextDep) { + removeSub(l) + } } } -export function trackEffect( - effect: ReactiveEffect, - dep: Dep, - debuggerEventExtraInfo?: DebuggerEventExtraInfo, -) { - if (dep.get(effect) !== effect._trackId) { - dep.set(effect, effect._trackId) - const oldDep = effect.deps[effect._depsLength] - if (oldDep !== dep) { - if (oldDep) { - cleanupDepEffect(oldDep, effect) - } - effect.deps[effect._depsLength++] = dep - } else { - effect._depsLength++ - } - if (__DEV__) { - effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!)) - } +function removeDep(link: Link) { + const prevDep = link.prevDep + const nextDep = link.nextDep + if (prevDep) { + prevDep.nextDep = nextDep + link.prevDep = undefined + } + if (nextDep) { + nextDep.prevDep = prevDep + link.nextDep = undefined } } -const queueEffectSchedulers: EffectScheduler[] = [] - -export function triggerEffects( - dep: Dep, - dirtyLevel: DirtyLevels, - debuggerEventExtraInfo?: DebuggerEventExtraInfo, -) { - pauseScheduling() - for (const effect of dep.keys()) { - // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result - let tracking: boolean | undefined - if ( - effect._dirtyLevel < dirtyLevel && - (tracking ??= dep.get(effect) === effect._trackId) - ) { - effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty - effect._dirtyLevel = dirtyLevel - } - if ( - effect._shouldSchedule && - (tracking ??= dep.get(effect) === effect._trackId) - ) { - if (__DEV__) { - effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo)) - } - effect.trigger() - if ( - (!effect._runnings || effect.allowRecurse) && - effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect - ) { - effect._shouldSchedule = false - if (effect.scheduler) { - queueEffectSchedulers.push(effect.scheduler) - } - } - } +export interface ReactiveEffectRunner { + (): T + effect: ReactiveEffect +} + +export function effect(fn: () => T): ReactiveEffectRunner { + const e = new ReactiveEffect(fn) + try { + e.run() + } catch (err) { + e.stop() + throw err } - resetScheduling() + const runner = e.run.bind(e) as ReactiveEffectRunner + runner.effect = e + return runner } diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 6a3eaec9fd5..24f664aa17a 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -1,4 +1,4 @@ -import type { ReactiveEffect } from './effect' +import type { ReactiveEffect } from './effect-old' import { warn } from './warning' let activeEffectScope: EffectScope | undefined diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 1c80fbc752b..0adaa49e040 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -19,7 +19,7 @@ export { type ShallowUnwrapRef, type RefUnwrapBailTypes, type CustomRefFactory, -} from './ref' +} from './ref-old' export { reactive, readonly, @@ -43,7 +43,7 @@ export { type WritableComputedOptions, type ComputedGetter, type ComputedSetter, -} from './computed' +} from './computed-old' export { deferredComputed } from './deferredComputed' export { effect, @@ -60,7 +60,7 @@ export { type DebuggerOptions, type DebuggerEvent, type DebuggerEventExtraInfo, -} from './effect' +} from './effect-old' export { trigger, track, ITERATE_KEY } from './reactiveEffect' export { effectScope, diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 8b94dd9a47f..f6784dfbc60 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -11,7 +11,7 @@ import { shallowCollectionHandlers, shallowReadonlyCollectionHandlers, } from './collectionHandlers' -import type { RawSymbol, Ref, UnwrapRefSimple } from './ref' +import type { RawSymbol, Ref, UnwrapRefSimple } from './ref-old' import { ReactiveFlags } from './constants' export interface Target { diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts index 6bf0e75115a..3a5166031ea 100644 --- a/packages/reactivity/src/reactiveEffect.ts +++ b/packages/reactivity/src/reactiveEffect.ts @@ -8,7 +8,7 @@ import { shouldTrack, trackEffect, triggerEffects, -} from './effect' +} from './effect-old' // The main WeakMap that stores {target -> key -> dep} connections. // Conceptually, it's easier to think of a dependency as a Dep class diff --git a/packages/reactivity/src/ref-old.ts b/packages/reactivity/src/ref-old.ts new file mode 100644 index 00000000000..ac9af63a7e9 --- /dev/null +++ b/packages/reactivity/src/ref-old.ts @@ -0,0 +1,532 @@ +import type { ComputedRef } from './computed-old' +import { + activeEffect, + shouldTrack, + trackEffect, + triggerEffects, +} from './effect-old' +import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' +import { + type IfAny, + hasChanged, + isArray, + isFunction, + isObject, +} from '@vue/shared' +import { + isProxy, + isReactive, + isReadonly, + isShallow, + toRaw, + toReactive, +} from './reactive' +import type { ShallowReactiveMarker } from './reactive' +import { type Dep, createDep } from './dep' +import { ComputedRefImpl } from './computed-old' +import { getDepFromReactive } from './reactiveEffect' + +declare const RefSymbol: unique symbol +export declare const RawSymbol: unique symbol + +export interface Ref { + value: T + /** + * Type differentiator only. + * We need this to be in public d.ts but don't want it to show up in IDE + * autocomplete, so we use a private Symbol instead. + */ + [RefSymbol]: true +} + +type RefBase = { + dep?: Dep + value: T +} + +export function trackRefValue(ref: RefBase) { + if (shouldTrack && activeEffect) { + ref = toRaw(ref) + trackEffect( + activeEffect, + (ref.dep ??= createDep( + () => (ref.dep = undefined), + ref instanceof ComputedRefImpl ? ref : undefined, + )), + __DEV__ + ? { + target: ref, + type: TrackOpTypes.GET, + key: 'value', + } + : void 0, + ) + } +} + +export function triggerRefValue( + ref: RefBase, + dirtyLevel: DirtyLevels = DirtyLevels.Dirty, + newVal?: any, +) { + ref = toRaw(ref) + const dep = ref.dep + if (dep) { + triggerEffects( + dep, + dirtyLevel, + __DEV__ + ? { + target: ref, + type: TriggerOpTypes.SET, + key: 'value', + newValue: newVal, + } + : void 0, + ) + } +} + +/** + * Checks if a value is a ref object. + * + * @param r - The value to inspect. + * @see {@link https://vuejs.org/api/reactivity-utilities.html#isref} + */ +export function isRef(r: Ref | unknown): r is Ref +export function isRef(r: any): r is Ref { + return !!(r && r.__v_isRef === true) +} + +/** + * Takes an inner value and returns a reactive and mutable ref object, which + * has a single property `.value` that points to the inner value. + * + * @param value - The object to wrap in the ref. + * @see {@link https://vuejs.org/api/reactivity-core.html#ref} + */ +export function ref(value: T): Ref> +export function ref(): Ref +export function ref(value?: unknown) { + return createRef(value, false) +} + +declare const ShallowRefMarker: unique symbol + +export type ShallowRef = Ref & { [ShallowRefMarker]?: true } + +/** + * Shallow version of {@link ref()}. + * + * @example + * ```js + * const state = shallowRef({ count: 1 }) + * + * // does NOT trigger change + * state.value.count = 2 + * + * // does trigger change + * state.value = { count: 2 } + * ``` + * + * @param value - The "inner value" for the shallow ref. + * @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref} + */ +export function shallowRef( + value: T, +): Ref extends T + ? T extends Ref + ? IfAny, T> + : ShallowRef + : ShallowRef +export function shallowRef(): ShallowRef +export function shallowRef(value?: unknown) { + return createRef(value, true) +} + +function createRef(rawValue: unknown, shallow: boolean) { + if (isRef(rawValue)) { + return rawValue + } + return new RefImpl(rawValue, shallow) +} + +class RefImpl { + private _value: T + private _rawValue: T + + public dep?: Dep = undefined + public readonly __v_isRef = true + + constructor( + value: T, + public readonly __v_isShallow: boolean, + ) { + this._rawValue = __v_isShallow ? value : toRaw(value) + this._value = __v_isShallow ? value : toReactive(value) + } + + get value() { + trackRefValue(this) + return this._value + } + + set value(newVal) { + const useDirectValue = + this.__v_isShallow || isShallow(newVal) || isReadonly(newVal) + newVal = useDirectValue ? newVal : toRaw(newVal) + if (hasChanged(newVal, this._rawValue)) { + this._rawValue = newVal + this._value = useDirectValue ? newVal : toReactive(newVal) + triggerRefValue(this, DirtyLevels.Dirty, newVal) + } + } +} + +/** + * Force trigger effects that depends on a shallow ref. This is typically used + * after making deep mutations to the inner value of a shallow ref. + * + * @example + * ```js + * const shallow = shallowRef({ + * greet: 'Hello, world' + * }) + * + * // Logs "Hello, world" once for the first run-through + * watchEffect(() => { + * console.log(shallow.value.greet) + * }) + * + * // This won't trigger the effect because the ref is shallow + * shallow.value.greet = 'Hello, universe' + * + * // Logs "Hello, universe" + * triggerRef(shallow) + * ``` + * + * @param ref - The ref whose tied effects shall be executed. + * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref} + */ +export function triggerRef(ref: Ref) { + triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0) +} + +export type MaybeRef = T | Ref +export type MaybeRefOrGetter = MaybeRef | (() => T) + +/** + * Returns the inner value if the argument is a ref, otherwise return the + * argument itself. This is a sugar function for + * `val = isRef(val) ? val.value : val`. + * + * @example + * ```js + * function useFoo(x: number | Ref) { + * const unwrapped = unref(x) + * // unwrapped is guaranteed to be number now + * } + * ``` + * + * @param ref - Ref or plain value to be converted into the plain value. + * @see {@link https://vuejs.org/api/reactivity-utilities.html#unref} + */ +export function unref(ref: MaybeRef | ComputedRef): T { + return isRef(ref) ? ref.value : ref +} + +/** + * Normalizes values / refs / getters to values. + * This is similar to {@link unref()}, except that it also normalizes getters. + * If the argument is a getter, it will be invoked and its return value will + * be returned. + * + * @example + * ```js + * toValue(1) // 1 + * toValue(ref(1)) // 1 + * toValue(() => 1) // 1 + * ``` + * + * @param source - A getter, an existing ref, or a non-function value. + * @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue} + */ +export function toValue(source: MaybeRefOrGetter | ComputedRef): T { + return isFunction(source) ? source() : unref(source) +} + +const shallowUnwrapHandlers: ProxyHandler = { + get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), + set: (target, key, value, receiver) => { + const oldValue = target[key] + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value + return true + } else { + return Reflect.set(target, key, value, receiver) + } + }, +} + +/** + * Returns a reactive proxy for the given object. + * + * If the object already is reactive, it's returned as-is. If not, a new + * reactive proxy is created. Direct child properties that are refs are properly + * handled, as well. + * + * @param objectWithRefs - Either an already-reactive object or a simple object + * that contains refs. + */ +export function proxyRefs( + objectWithRefs: T, +): ShallowUnwrapRef { + return isReactive(objectWithRefs) + ? objectWithRefs + : new Proxy(objectWithRefs, shallowUnwrapHandlers) +} + +export type CustomRefFactory = ( + track: () => void, + trigger: () => void, +) => { + get: () => T + set: (value: T) => void +} + +class CustomRefImpl { + public dep?: Dep = undefined + + private readonly _get: ReturnType>['get'] + private readonly _set: ReturnType>['set'] + + public readonly __v_isRef = true + + constructor(factory: CustomRefFactory) { + const { get, set } = factory( + () => trackRefValue(this), + () => triggerRefValue(this), + ) + this._get = get + this._set = set + } + + get value() { + return this._get() + } + + set value(newVal) { + this._set(newVal) + } +} + +/** + * Creates a customized ref with explicit control over its dependency tracking + * and updates triggering. + * + * @param factory - The function that receives the `track` and `trigger` callbacks. + * @see {@link https://vuejs.org/api/reactivity-advanced.html#customref} + */ +export function customRef(factory: CustomRefFactory): Ref { + return new CustomRefImpl(factory) as any +} + +export type ToRefs = { + [K in keyof T]: ToRef +} + +/** + * Converts a reactive object to a plain object where each property of the + * resulting object is a ref pointing to the corresponding property of the + * original object. Each individual ref is created using {@link toRef()}. + * + * @param object - Reactive object to be made into an object of linked refs. + * @see {@link https://vuejs.org/api/reactivity-utilities.html#torefs} + */ +export function toRefs(object: T): ToRefs { + if (__DEV__ && !isProxy(object)) { + console.warn(`toRefs() expects a reactive object but received a plain one.`) + } + const ret: any = isArray(object) ? new Array(object.length) : {} + for (const key in object) { + ret[key] = propertyToRef(object, key) + } + return ret +} + +class ObjectRefImpl { + public readonly __v_isRef = true + + constructor( + private readonly _object: T, + private readonly _key: K, + private readonly _defaultValue?: T[K], + ) {} + + get value() { + const val = this._object[this._key] + return val === undefined ? this._defaultValue! : val + } + + set value(newVal) { + this._object[this._key] = newVal + } + + get dep(): Dep | undefined { + return getDepFromReactive(toRaw(this._object), this._key) + } +} + +class GetterRefImpl { + public readonly __v_isRef = true + public readonly __v_isReadonly = true + constructor(private readonly _getter: () => T) {} + get value() { + return this._getter() + } +} + +export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> + +/** + * Used to normalize values / refs / getters into refs. + * + * @example + * ```js + * // returns existing refs as-is + * toRef(existingRef) + * + * // creates a ref that calls the getter on .value access + * toRef(() => props.foo) + * + * // creates normal refs from non-function values + * // equivalent to ref(1) + * toRef(1) + * ``` + * + * Can also be used to create a ref for a property on a source reactive object. + * The created ref is synced with its source property: mutating the source + * property will update the ref, and vice-versa. + * + * @example + * ```js + * const state = reactive({ + * foo: 1, + * bar: 2 + * }) + * + * const fooRef = toRef(state, 'foo') + * + * // mutating the ref updates the original + * fooRef.value++ + * console.log(state.foo) // 2 + * + * // mutating the original also updates the ref + * state.foo++ + * console.log(fooRef.value) // 3 + * ``` + * + * @param source - A getter, an existing ref, a non-function value, or a + * reactive object to create a property ref from. + * @param [key] - (optional) Name of the property in the reactive object. + * @see {@link https://vuejs.org/api/reactivity-utilities.html#toref} + */ +export function toRef( + value: T, +): T extends () => infer R + ? Readonly> + : T extends Ref + ? T + : Ref> +export function toRef( + object: T, + key: K, +): ToRef +export function toRef( + object: T, + key: K, + defaultValue: T[K], +): ToRef> +export function toRef( + source: Record | MaybeRef, + key?: string, + defaultValue?: unknown, +): Ref { + if (isRef(source)) { + return source + } else if (isFunction(source)) { + return new GetterRefImpl(source) as any + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key!, defaultValue) + } else { + return ref(source) + } +} + +function propertyToRef( + source: Record, + key: string, + defaultValue?: unknown, +) { + const val = source[key] + return isRef(val) + ? val + : (new ObjectRefImpl(source, key, defaultValue) as any) +} + +// corner case when use narrows type +// Ex. type RelativePath = string & { __brand: unknown } +// RelativePath extends object -> true +type BaseTypes = string | number | boolean + +/** + * This is a special exported interface for other packages to declare + * additional types that should bail out for ref unwrapping. For example + * \@vue/runtime-dom can declare it like so in its d.ts: + * + * ``` ts + * declare module '@vue/reactivity' { + * export interface RefUnwrapBailTypes { + * runtimeDOMBailTypes: Node | Window + * } + * } + * ``` + */ +export interface RefUnwrapBailTypes {} + +export type ShallowUnwrapRef = { + [K in keyof T]: DistrubuteRef +} + +type DistrubuteRef = T extends Ref ? V : T + +export type UnwrapRef = + T extends ShallowRef + ? V + : T extends Ref + ? UnwrapRefSimple + : UnwrapRefSimple + +export type UnwrapRefSimple = T extends + | Function + | BaseTypes + | Ref + | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] + | { [RawSymbol]?: true } + ? T + : T extends Map + ? Map> & UnwrapRef>> + : T extends WeakMap + ? WeakMap> & + UnwrapRef>> + : T extends Set + ? Set> & UnwrapRef>> + : T extends WeakSet + ? WeakSet> & UnwrapRef>> + : T extends ReadonlyArray + ? { [K in keyof T]: UnwrapRefSimple } + : T extends object & { [ShallowReactiveMarker]?: never } + ? { + [P in keyof T]: P extends symbol ? T[P] : UnwrapRef + } + : T diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 1b9d60ef06b..27c8c51a2b3 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -1,11 +1,3 @@ -import type { ComputedRef } from './computed' -import { - activeEffect, - shouldTrack, - trackEffect, - triggerEffects, -} from './effect' -import { DirtyLevels, TrackOpTypes, TriggerOpTypes } from './constants' import { type IfAny, hasChanged, @@ -13,7 +5,9 @@ import { isFunction, isObject, } from '@vue/shared' +import { Dep } from './effect' import { + type ShallowReactiveMarker, isProxy, isReactive, isReadonly, @@ -21,10 +15,7 @@ import { toRaw, toReactive, } from './reactive' -import type { ShallowReactiveMarker } from './reactive' -import { type Dep, createDep } from './dep' -import { ComputedRefImpl } from './computed' -import { getDepFromReactive } from './reactiveEffect' +import type { ComputedRef } from './computed' declare const RefSymbol: unique symbol export declare const RawSymbol: unique symbol @@ -39,54 +30,6 @@ export interface Ref { [RefSymbol]: true } -type RefBase = { - dep?: Dep - value: T -} - -export function trackRefValue(ref: RefBase) { - if (shouldTrack && activeEffect) { - ref = toRaw(ref) - trackEffect( - activeEffect, - (ref.dep ??= createDep( - () => (ref.dep = undefined), - ref instanceof ComputedRefImpl ? ref : undefined, - )), - __DEV__ - ? { - target: ref, - type: TrackOpTypes.GET, - key: 'value', - } - : void 0, - ) - } -} - -export function triggerRefValue( - ref: RefBase, - dirtyLevel: DirtyLevels = DirtyLevels.Dirty, - newVal?: any, -) { - ref = toRaw(ref) - const dep = ref.dep - if (dep) { - triggerEffects( - dep, - dirtyLevel, - __DEV__ - ? { - target: ref, - type: TriggerOpTypes.SET, - key: 'value', - newValue: newVal, - } - : void 0, - ) - } -} - /** * Checks if a value is a ref object. * @@ -151,11 +94,12 @@ function createRef(rawValue: unknown, shallow: boolean) { return new RefImpl(rawValue, shallow) } -class RefImpl { +class RefImpl { private _value: T private _rawValue: T - public dep?: Dep = undefined + dep: Dep = new Dep() + public readonly __v_isRef = true constructor( @@ -167,7 +111,7 @@ class RefImpl { } get value() { - trackRefValue(this) + this.dep.track() return this._value } @@ -178,7 +122,7 @@ class RefImpl { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = useDirectValue ? newVal : toReactive(newVal) - triggerRefValue(this, DirtyLevels.Dirty, newVal) + this.dep.trigger() } } } @@ -209,7 +153,7 @@ class RefImpl { * @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref} */ export function triggerRef(ref: Ref) { - triggerRefValue(ref, DirtyLevels.Dirty, __DEV__ ? ref.value : void 0) + ;(ref as unknown as RefImpl).dep.trigger() } export type MaybeRef = T | Ref @@ -295,7 +239,7 @@ export type CustomRefFactory = ( } class CustomRefImpl { - public dep?: Dep = undefined + public dep: Dep private readonly _get: ReturnType>['get'] private readonly _set: ReturnType>['set'] @@ -303,10 +247,8 @@ class CustomRefImpl { public readonly __v_isRef = true constructor(factory: CustomRefFactory) { - const { get, set } = factory( - () => trackRefValue(this), - () => triggerRefValue(this), - ) + const dep = (this.dep = new Dep()) + const { get, set } = factory(dep.track.bind(dep), dep.trigger.bind(dep)) this._get = get this._set = set } @@ -373,6 +315,7 @@ class ObjectRefImpl { } get dep(): Dep | undefined { + // @ts-expect-error TODO return getDepFromReactive(toRaw(this._object), this._key) } } From f9579b0deab3059b7fef77a5302426daca299b04 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 21 Feb 2024 17:52:54 +0800 Subject: [PATCH 02/43] wip: save --- packages/reactivity/src/computed.ts | 87 +++++++++++++++++++++++++++-- packages/reactivity/src/effect.ts | 51 ++++++++++++++++- 2 files changed, 131 insertions(+), 7 deletions(-) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 2fcc351d77c..fe932129ab2 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,4 +1,6 @@ +import { isFunction } from '@vue/shared' import { + type DebuggerOptions, Dep, Flags, type Link, @@ -7,6 +9,7 @@ import { refreshComputed, } from './effect' import type { Ref } from './ref' +import { warn } from './warning' declare const ComputedRefSymbol: unique symbol @@ -27,7 +30,7 @@ export interface WritableComputedOptions { set: ComputedSetter } -export class ComputedRefImpl implements Subscriber { +export class ComputedRefImpl implements Subscriber { // A computed is a ref _value: any = undefined dep: Dep @@ -36,7 +39,12 @@ export class ComputedRefImpl implements Subscriber { // track variaous states flags = Flags.DIRTY - constructor(public getter: ComputedGetter) { + constructor( + public getter: ComputedGetter, + private readonly _setter: ComputedSetter | undefined, + // @ts-expect-error TODO + private isSSR: boolean, + ) { this.dep = new Dep(this) } @@ -56,8 +64,79 @@ export class ComputedRefImpl implements Subscriber { } return this._value } + + set value(newValue) { + if (this._setter) { + this._setter(newValue) + } else if (__DEV__) { + warn('Write operation failed: computed value is readonly') + } + } } -export function computed(getter: ComputedGetter) { - return new ComputedRefImpl(getter) +/** + * Takes a getter function and returns a readonly reactive ref object for the + * returned value from the getter. It can also take an object with get and set + * functions to create a writable ref object. + * + * @example + * ```js + * // Creating a readonly computed ref: + * const count = ref(1) + * const plusOne = computed(() => count.value + 1) + * + * console.log(plusOne.value) // 2 + * plusOne.value++ // error + * ``` + * + * ```js + * // Creating a writable computed ref: + * const count = ref(1) + * const plusOne = computed({ + * get: () => count.value + 1, + * set: (val) => { + * count.value = val - 1 + * } + * }) + * + * plusOne.value = 1 + * console.log(count.value) // 0 + * ``` + * + * @param getter - Function that produces the next value. + * @param debugOptions - For debugging. See {@link https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging}. + * @see {@link https://vuejs.org/api/reactivity-core.html#computed} + */ +export function computed( + getter: ComputedGetter, + debugOptions?: DebuggerOptions, +): ComputedRef +export function computed( + options: WritableComputedOptions, + debugOptions?: DebuggerOptions, +): WritableComputedRef +export function computed( + getterOrOptions: ComputedGetter | WritableComputedOptions, + debugOptions?: DebuggerOptions, + isSSR = false, +) { + let getter: ComputedGetter + let setter: ComputedSetter | undefined + + if (isFunction(getterOrOptions)) { + getter = getterOrOptions + } else { + getter = getterOrOptions.get + setter = getterOrOptions.set + } + + const cRef = new ComputedRefImpl(getter, setter, isSSR) + + // TODO + if (__DEV__ && debugOptions && !isSSR) { + // cRef.effect.onTrack = debugOptions.onTrack + // cRef.effect.onTrigger = debugOptions.onTrigger + } + + return cRef as any } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index f8dae5c112c..adde7155bc7 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,4 +1,39 @@ import type { ComputedRefImpl } from './computed' +import type { TrackOpTypes, TriggerOpTypes } from './constants' +import type { EffectScope } from './effectScope' + +export type EffectScheduler = (run: () => any) => any + +export type DebuggerEvent = { + effect: ReactiveEffect +} & DebuggerEventExtraInfo + +export type DebuggerEventExtraInfo = { + target: object + type: TrackOpTypes | TriggerOpTypes + key: any + newValue?: any + oldValue?: any + oldTarget?: Map | Set +} + +export interface DebuggerOptions { + onTrack?: (event: DebuggerEvent) => void + onTrigger?: (event: DebuggerEvent) => void +} + +export interface ReactiveEffectOptions extends DebuggerOptions { + lazy?: boolean + scheduler?: EffectScheduler + scope?: EffectScope + allowRecurse?: boolean + onStop?: () => void +} + +export interface ReactiveEffectRunner { + (): T + effect: ReactiveEffect +} export let activeSub: Subscriber | undefined @@ -159,17 +194,27 @@ export class Dep { } } -export type EffectScheduler = (run: () => any) => any - export class ReactiveEffect implements Subscriber { + /** + * @internal + */ deps?: Link = undefined + /** + * @internal + */ flags: Flags = Flags.ACTIVE | Flags.TRACKING + /** + * @internal + */ + nextEffect?: ReactiveEffect = undefined scheduler?: EffectScheduler = undefined - nextEffect?: ReactiveEffect = undefined constructor(private _fn: () => any) {} + /** + * @internal + */ notify() { if (!(this.flags & Flags.NOTIFIED)) { this.flags |= Flags.NOTIFIED From bb078943f4de4b247975efd6ed94ef100649742d Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 21 Feb 2024 18:28:23 +0800 Subject: [PATCH 03/43] wip: save --- .../reactivity/__tests__/computed.bench.ts | 3 +- .../reactivity/__tests__/computed.spec.ts | 43 +++---- .../__tests__/deferredComputed.spec.ts | 1 + .../reactivity/__tests__/reactive.spec.ts | 6 +- packages/reactivity/src/baseHandlers.ts | 2 +- packages/reactivity/src/computed-old.ts | 2 +- packages/reactivity/src/computed.ts | 13 +- packages/reactivity/src/dep-old.ts | 17 +++ packages/reactivity/src/dep.ts | 113 +++++++++++++++--- packages/reactivity/src/effect-old.ts | 2 +- packages/reactivity/src/effect.ts | 112 ++--------------- packages/reactivity/src/index.ts | 4 +- packages/reactivity/src/reactive.ts | 2 +- packages/reactivity/src/reactiveEffect.ts | 2 +- packages/reactivity/src/ref-old.ts | 2 +- packages/reactivity/src/ref.ts | 2 +- .../__tests__/apiSetupHelpers.spec.ts | 2 + 17 files changed, 162 insertions(+), 166 deletions(-) create mode 100644 packages/reactivity/src/dep-old.ts diff --git a/packages/reactivity/__tests__/computed.bench.ts b/packages/reactivity/__tests__/computed.bench.ts index 0ffa288ff1e..c4cb46088bf 100644 --- a/packages/reactivity/__tests__/computed.bench.ts +++ b/packages/reactivity/__tests__/computed.bench.ts @@ -1,5 +1,6 @@ import { bench, describe } from 'vitest' -import { type ComputedRef, type Ref, computed, ref } from '../src/index' +import { type ComputedRef, computed } from '../src/computed' +import { type Ref, ref } from '../src/ref' describe('computed', () => { bench('create computed', () => { diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 0e8038d24b1..d8570f93d15 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -13,7 +13,6 @@ import { shallowRef, toRaw, } from '../src' -import { DirtyLevels } from '../src/constants' import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed-old' describe('reactivity/computed', () => { @@ -133,6 +132,7 @@ describe('reactivity/computed', () => { expect(dummy).toBe(undefined) value.foo = 1 expect(dummy).toBe(1) + // @ts-expect-error TODO cValue.effect.stop() value.foo = 2 expect(dummy).toBe(1) @@ -221,6 +221,7 @@ describe('reactivity/computed', () => { it('should expose value when stopped', () => { const x = computed(() => 1) + // @ts-expect-error TODO x.effect.stop() expect(x.value).toBe(1) }) @@ -231,6 +232,7 @@ describe('reactivity/computed', () => { events.push(e) }) const obj = reactive({ foo: 1, bar: 2 }) + // @ts-expect-error TODO const c = computed(() => (obj.foo, 'bar' in obj, Object.keys(obj)), { onTrack, }) @@ -238,18 +240,21 @@ describe('reactivity/computed', () => { expect(onTrack).toHaveBeenCalledTimes(3) expect(events).toEqual([ { + // @ts-expect-error TODO effect: c.effect, target: toRaw(obj), type: TrackOpTypes.GET, key: 'foo', }, { + // @ts-expect-error TODO effect: c.effect, target: toRaw(obj), type: TrackOpTypes.HAS, key: 'bar', }, { + // @ts-expect-error TODO effect: c.effect, target: toRaw(obj), type: TrackOpTypes.ITERATE, @@ -264,6 +269,7 @@ describe('reactivity/computed', () => { events.push(e) }) const obj = reactive<{ foo?: number }>({ foo: 1 }) + // @ts-expect-error TODO const c = computed(() => obj.foo, { onTrigger }) // computed won't trigger compute until accessed @@ -273,6 +279,7 @@ describe('reactivity/computed', () => { expect(c.value).toBe(2) expect(onTrigger).toHaveBeenCalledTimes(1) expect(events[0]).toEqual({ + // @ts-expect-error TODO effect: c.effect, target: toRaw(obj), type: TriggerOpTypes.SET, @@ -285,6 +292,7 @@ describe('reactivity/computed', () => { expect(c.value).toBeUndefined() expect(onTrigger).toHaveBeenCalledTimes(2) expect(events[1]).toEqual({ + // @ts-expect-error TODO effect: c.effect, target: toRaw(obj), type: TriggerOpTypes.DELETE, @@ -404,9 +412,13 @@ describe('reactivity/computed', () => { a.value++ e.value + // @ts-expect-error TODO expect(e.effect.deps.length).toBe(3) + // @ts-expect-error TODO expect(e.effect.deps.indexOf((b as any).dep)).toBe(0) + // @ts-expect-error TODO expect(e.effect.deps.indexOf((d as any).dep)).toBe(1) + // @ts-expect-error TODO expect(e.effect.deps.indexOf((c as any).dep)).toBe(2) expect(cSpy).toHaveBeenCalledTimes(2) @@ -461,12 +473,11 @@ describe('reactivity/computed', () => { const c1 = computed(() => v.value) const c2 = computed(() => c1.value) + // @ts-expect-error TODO c1.effect.allowRecurse = true + // @ts-expect-error TODO c2.effect.allowRecurse = true c2.value - - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) }) it('should chained computeds dirtyLevel update with first computed effect', () => { @@ -481,14 +492,6 @@ describe('reactivity/computed', () => { const c3 = computed(() => c2.value) c3.value - - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe( - DirtyLevels.MaybeDirty_ComputedSideEffect, - ) - expect(c3.effect._dirtyLevel).toBe( - DirtyLevels.MaybeDirty_ComputedSideEffect, - ) expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() }) @@ -502,7 +505,6 @@ describe('reactivity/computed', () => { }) const c2 = computed(() => v.value + c1.value) expect(c2.value).toBe('0foo') - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(c2.value).toBe('1foo') expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() }) @@ -523,8 +525,6 @@ describe('reactivity/computed', () => { c2.value }) expect(fnSpy).toBeCalledTimes(1) - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) v.value = 2 expect(fnSpy).toBeCalledTimes(2) expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() @@ -553,22 +553,9 @@ describe('reactivity/computed', () => { c3.value v2.value = true - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) c3.value - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe( - DirtyLevels.MaybeDirty_ComputedSideEffect, - ) - expect(c3.effect._dirtyLevel).toBe( - DirtyLevels.MaybeDirty_ComputedSideEffect, - ) - v1.value.v.value = 999 - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) - expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) expect(c3.value).toBe('yes') expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() diff --git a/packages/reactivity/__tests__/deferredComputed.spec.ts b/packages/reactivity/__tests__/deferredComputed.spec.ts index 8e78ba959c3..c3692109424 100644 --- a/packages/reactivity/__tests__/deferredComputed.spec.ts +++ b/packages/reactivity/__tests__/deferredComputed.spec.ts @@ -147,6 +147,7 @@ describe('deferred computed', () => { effect(() => c1.value) expect(c1Spy).toHaveBeenCalledTimes(1) + // @ts-expect-error TODO c1.effect.stop() // trigger src.value++ diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index a9f42586220..ab953ff891a 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -1,7 +1,7 @@ -import { isRef, ref } from '../src/ref-old' +import { isRef, ref } from '../src/ref' import { isReactive, markRaw, reactive, toRaw } from '../src/reactive' -import { computed } from '../src/computed-old' -import { effect } from '../src/effect-old' +import { computed } from '../src/computed' +import { effect } from '../src/effect' describe('reactivity/reactive', () => { test('Object', () => { diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 3282a020077..d2342cd9e78 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -27,7 +27,7 @@ import { isSymbol, makeMap, } from '@vue/shared' -import { isRef } from './ref-old' +import { isRef } from './ref' import { warn } from './warning' const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`) diff --git a/packages/reactivity/src/computed-old.ts b/packages/reactivity/src/computed-old.ts index da92dd155b1..a6e6cb2c394 100644 --- a/packages/reactivity/src/computed-old.ts +++ b/packages/reactivity/src/computed-old.ts @@ -2,7 +2,7 @@ import { type DebuggerOptions, ReactiveEffect } from './effect-old' import { type Ref, trackRefValue, triggerRefValue } from './ref-old' import { NOOP, hasChanged, isFunction } from '@vue/shared' import { toRaw } from './reactive' -import type { Dep } from './dep' +import type { Dep } from './dep-old' import { DirtyLevels, ReactiveFlags } from './constants' import { warn } from './warning' diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index fe932129ab2..ac9dd4a6c84 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,15 +1,14 @@ import { isFunction } from '@vue/shared' import { type DebuggerOptions, - Dep, Flags, type Link, - type ReactiveEffect, type Subscriber, refreshComputed, } from './effect' import type { Ref } from './ref' import { warn } from './warning' +import { Dep } from './dep' declare const ComputedRefSymbol: unique symbol @@ -18,9 +17,7 @@ export interface ComputedRef extends WritableComputedRef { [ComputedRefSymbol]: true } -export interface WritableComputedRef extends Ref { - readonly effect: ReactiveEffect -} +export interface WritableComputedRef extends Ref {} export type ComputedGetter = (oldValue?: T) => T export type ComputedSetter = (newValue: T) => void @@ -33,7 +30,7 @@ export interface WritableComputedOptions { export class ComputedRefImpl implements Subscriber { // A computed is a ref _value: any = undefined - dep: Dep + dep = new Dep(this) // A computed is also a subscriber that tracks other deps deps?: Link = undefined // track variaous states @@ -44,9 +41,7 @@ export class ComputedRefImpl implements Subscriber { private readonly _setter: ComputedSetter | undefined, // @ts-expect-error TODO private isSSR: boolean, - ) { - this.dep = new Dep(this) - } + ) {} notify() { if (!(this.flags & Flags.NOTIFIED)) { diff --git a/packages/reactivity/src/dep-old.ts b/packages/reactivity/src/dep-old.ts new file mode 100644 index 00000000000..52a44352790 --- /dev/null +++ b/packages/reactivity/src/dep-old.ts @@ -0,0 +1,17 @@ +import type { ReactiveEffect } from './effect-old' +import type { ComputedRefImpl } from './computed-old' + +export type Dep = Map & { + cleanup: () => void + computed?: ComputedRefImpl +} + +export const createDep = ( + cleanup: () => void, + computed?: ComputedRefImpl, +): Dep => { + const dep = new Map() as Dep + dep.cleanup = cleanup + dep.computed = computed + return dep +} diff --git a/packages/reactivity/src/dep.ts b/packages/reactivity/src/dep.ts index 52a44352790..03eba8187e9 100644 --- a/packages/reactivity/src/dep.ts +++ b/packages/reactivity/src/dep.ts @@ -1,17 +1,102 @@ -import type { ReactiveEffect } from './effect-old' -import type { ComputedRefImpl } from './computed-old' +import type { ComputedRefImpl } from './computed' +import { Flags, type Link, activeSub, endBatch, startBatch } from './effect' -export type Dep = Map & { - cleanup: () => void - computed?: ComputedRefImpl -} +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 + + constructor(public computed?: ComputedRefImpl) {} + + track(): Link | undefined { + if (activeSub === undefined) { + return + } + + let link = this.activeLink + if (link === undefined || link.sub !== activeSub) { + link = this.activeLink = { + dep: this, + sub: activeSub, + version: this.version, + nextDep: undefined, + prevDep: activeSub.deps, + nextSub: undefined, + prevSub: undefined, + prevActiveLink: undefined, + } + + // add the link to the activeEffect as a dep (as tail) + if (activeSub.deps) { + activeSub.deps.nextDep = link + } + activeSub.deps = link + + // add the link to this dep as a subscriber (as tail) + if (activeSub.flags & Flags.TRACKING) { + const computed = this.computed + if (computed && !this.subs) { + // a computed dep getting its first subscriber, enable tracking + + // lazily subscribe to all its deps + computed.flags |= Flags.TRACKING | Flags.DIRTY + for (let l = computed.deps; l !== undefined; l = l.nextDep) { + l.dep.addSub(l) + } + } + this.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) { + link.nextDep.prevDep = link.prevDep + if (link.prevDep) { + link.prevDep.nextDep = link.nextDep + } + + link.prevDep = activeSub.deps + link.nextDep = undefined + + activeSub.deps!.nextDep = link + activeSub.deps = link + } + } + return link + } + + addSub(link: Link) { + if (this.subs !== link) { + link.prevSub = this.subs + if (this.subs) { + this.subs.nextSub = link + } + this.subs = link + } + } + + trigger() { + this.version++ + this.notify() + } -export const createDep = ( - cleanup: () => void, - computed?: ComputedRefImpl, -): Dep => { - const dep = new Map() as Dep - dep.cleanup = cleanup - dep.computed = computed - return dep + notify() { + startBatch() + try { + for (let link = this.subs; link !== undefined; link = link.prevSub) { + link.sub.notify() + } + } finally { + endBatch() + } + } } diff --git a/packages/reactivity/src/effect-old.ts b/packages/reactivity/src/effect-old.ts index 1375c463da6..2e13627e859 100644 --- a/packages/reactivity/src/effect-old.ts +++ b/packages/reactivity/src/effect-old.ts @@ -5,7 +5,7 @@ import { type TrackOpTypes, type TriggerOpTypes, } from './constants' -import type { Dep } from './dep' +import type { Dep } from './dep-old' import { type EffectScope, recordEffectScope } from './effectScope' export type EffectScheduler = (...args: any[]) => any diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index adde7155bc7..a97854cb1b8 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -1,5 +1,6 @@ import type { ComputedRefImpl } from './computed' import type { TrackOpTypes, TriggerOpTypes } from './constants' +import type { Dep } from './dep' import type { EffectScope } from './effectScope' export type EffectScheduler = (run: () => any) => any @@ -94,106 +95,6 @@ export interface Link { prevActiveLink?: Link } -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 - - constructor(public computed?: ComputedRefImpl) {} - - track(): Link | undefined { - if (activeSub === undefined) { - return - } - - let link = this.activeLink - if (link === undefined || link.sub !== activeSub) { - link = this.activeLink = { - dep: this, - sub: activeSub, - version: this.version, - nextDep: undefined, - prevDep: activeSub.deps, - nextSub: undefined, - prevSub: undefined, - prevActiveLink: undefined, - } - - // add the link to the activeEffect as a dep (as tail) - if (activeSub.deps) { - activeSub.deps.nextDep = link - } - activeSub.deps = link - - // add the link to this dep as a subscriber (as tail) - if (activeSub.flags & Flags.TRACKING) { - const computed = this.computed - if (computed && !this.subs) { - // a computed dep getting its first subscriber, enable tracking + - // lazily subscribe to all its deps - computed.flags |= Flags.TRACKING | Flags.DIRTY - for (let l = computed.deps; l !== undefined; l = l.nextDep) { - l.dep.addSub(l) - } - } - this.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) { - link.nextDep.prevDep = link.prevDep - if (link.prevDep) { - link.prevDep.nextDep = link.nextDep - } - - link.prevDep = activeSub.deps - link.nextDep = undefined - - activeSub.deps!.nextDep = link - activeSub.deps = link - } - } - return link - } - - addSub(link: Link) { - if (this.subs !== link) { - link.prevSub = this.subs - if (this.subs) { - this.subs.nextSub = link - } - this.subs = link - } - } - - trigger() { - this.version++ - this.notify() - } - - notify() { - batchDepth++ - try { - for (let link = this.subs; link !== undefined; link = link.prevSub) { - link.sub.notify() - } - } finally { - runBatchedEffects() - } - } -} - export class ReactiveEffect implements Subscriber { /** * @internal @@ -248,7 +149,7 @@ export class ReactiveEffect implements Subscriber { cleanupDeps(this) activeSub = prevEffect this.flags &= ~Flags.RUNNING - runBatchedEffects() + endBatch() } } @@ -266,7 +167,14 @@ export class ReactiveEffect implements Subscriber { let batchDepth = 0 let batchedEffect: ReactiveEffect | undefined -function runBatchedEffects() { +export function startBatch() { + batchDepth++ +} + +/** + * Run batched effects when all batches have ended + */ +export function endBatch() { if (batchDepth > 1) { batchDepth-- return diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 0adaa49e040..133b6254baf 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -19,7 +19,7 @@ export { type ShallowUnwrapRef, type RefUnwrapBailTypes, type CustomRefFactory, -} from './ref-old' +} from './ref' export { reactive, readonly, @@ -43,7 +43,7 @@ export { type WritableComputedOptions, type ComputedGetter, type ComputedSetter, -} from './computed-old' +} from './computed' export { deferredComputed } from './deferredComputed' export { effect, diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index f6784dfbc60..8b94dd9a47f 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -11,7 +11,7 @@ import { shallowCollectionHandlers, shallowReadonlyCollectionHandlers, } from './collectionHandlers' -import type { RawSymbol, Ref, UnwrapRefSimple } from './ref-old' +import type { RawSymbol, Ref, UnwrapRefSimple } from './ref' import { ReactiveFlags } from './constants' export interface Target { diff --git a/packages/reactivity/src/reactiveEffect.ts b/packages/reactivity/src/reactiveEffect.ts index 3a5166031ea..0a05242a953 100644 --- a/packages/reactivity/src/reactiveEffect.ts +++ b/packages/reactivity/src/reactiveEffect.ts @@ -1,6 +1,6 @@ import { isArray, isIntegerKey, isMap, isSymbol } from '@vue/shared' import { DirtyLevels, type TrackOpTypes, TriggerOpTypes } from './constants' -import { type Dep, createDep } from './dep' +import { type Dep, createDep } from './dep-old' import { activeEffect, pauseScheduling, diff --git a/packages/reactivity/src/ref-old.ts b/packages/reactivity/src/ref-old.ts index ac9af63a7e9..47af7c4cfee 100644 --- a/packages/reactivity/src/ref-old.ts +++ b/packages/reactivity/src/ref-old.ts @@ -22,7 +22,7 @@ import { toReactive, } from './reactive' import type { ShallowReactiveMarker } from './reactive' -import { type Dep, createDep } from './dep' +import { type Dep, createDep } from './dep-old' import { ComputedRefImpl } from './computed-old' import { getDepFromReactive } from './reactiveEffect' diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 27c8c51a2b3..3d55dac1e36 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -5,7 +5,7 @@ import { isFunction, isObject, } from '@vue/shared' -import { Dep } from './effect' +import { Dep } from './dep' import { type ShallowReactiveMarker, isProxy, diff --git a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts index 04e9c1c86db..9c3d20680e7 100644 --- a/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupHelpers.spec.ts @@ -447,9 +447,11 @@ describe('SFC