diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c7963499f..e70565f06 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -1150,4 +1150,54 @@ describe('reactivity/computed', () => { const t2 = performance.now() expect(t2 - t1).toBeLessThan(process.env.CI ? 100 : 30) }) + + describe('peek', () => { + it('should return updated value', () => { + const value = reactive<{ foo?: number }>({}) + const cValue = computed(() => value.foo) + expect(cValue.peek()).toBe(undefined) + value.foo = 1 + expect(cValue.peek()).toBe(1) + }) + + it('should compute lazily', () => { + const value = reactive<{ foo?: number }>({}) + const getter = vi.fn(() => value.foo) + const cValue = computed(getter) + + // lazy + expect(getter).not.toHaveBeenCalled() + + expect(cValue.peek()).toBe(undefined) + expect(getter).toHaveBeenCalledTimes(1) + + // should not compute again + cValue.peek() + expect(getter).toHaveBeenCalledTimes(1) + + // should not compute until needed + value.foo = 1 + expect(getter).toHaveBeenCalledTimes(1) + + // now it should compute + expect(cValue.peek()).toBe(1) + expect(getter).toHaveBeenCalledTimes(2) + + // should not compute again + cValue.peek() + expect(getter).toHaveBeenCalledTimes(2) + }) + + it('should not trigger effect', () => { + const value = reactive<{ foo?: number }>({}) + const cValue = computed(() => value.foo) + let dummy + effect(() => { + dummy = cValue.peek() + }) + expect(dummy).toBe(undefined) + value.foo = 1 + expect(dummy).toBe(undefined) + }) + }) }) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 242fc7071..63e93de9a 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -27,6 +27,7 @@ import { pauseTracking, resetTracking, startBatch, + untrack, } from '../src/effect' describe('reactivity/effect', () => { @@ -1182,6 +1183,17 @@ describe('reactivity/effect', () => { expect(spy2).toHaveBeenCalledTimes(2) }) + it('should not track dependencies when using untrack', () => { + const value = ref(1) + let dummy + effect(() => { + dummy = untrack(() => value.value) + }) + expect(dummy).toBe(1) + value.value = 2 + expect(dummy).toBe(1) + }) + describe('dep unsubscribe', () => { function getSubCount(dep: Dep | undefined) { let count = 0 diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 7976a5373..e2530d9b5 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -534,4 +534,26 @@ describe('reactivity/ref', () => { // @ts-expect-error internal field expect(objectRefValue._value).toBe(1) }) + + describe('peek', () => { + it('should hold a value', () => { + const a = ref(1) + expect(a.peek()).toBe(1) + a.value = 2 + expect(a.peek()).toBe(2) + }) + + it('should not be reactive', () => { + const a = ref(1) + let dummy + const fn = vi.fn(() => { + dummy = a.peek() + }) + effect(fn) + expect(fn).toHaveBeenCalledTimes(1) + expect(dummy).toBe(1) + a.value = 2 + expect(fn).toHaveBeenCalledTimes(1) + }) + }) }) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index ea798e201..eec4545f9 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -26,9 +26,11 @@ interface BaseComputedRef extends Ref { export interface ComputedRef extends BaseComputedRef { readonly value: T + peek: () => T } export interface WritableComputedRef extends BaseComputedRef { + peek: () => T [WritableComputedRefSymbol]: true } @@ -151,6 +153,12 @@ export class ComputedRefImpl implements Subscriber { warn('Write operation failed: computed value is readonly') } } + + peek(): T { + refreshComputed(this) + + return this._value + } } /** diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 84cf184c1..1c6e6cb84 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -537,6 +537,15 @@ export function resetTracking(): void { shouldTrack = last === undefined ? true : last } +export function untrack(fn: () => T): T { + try { + pauseTracking() + return fn() + } finally { + resetTracking() + } +} + /** * Registers a cleanup function for the current active effect. * The cleanup function is called right before the next effect run, or when the diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index f0445e87d..9bc9b8af3 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -55,6 +55,7 @@ export { enableTracking, pauseTracking, resetTracking, + untrack, onEffectCleanup, ReactiveEffect, EffectFlags, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 59b713dd8..738ad8f5c 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -34,6 +34,10 @@ export interface Ref { [RefSymbol]: true } +export interface RefWithPeek extends Ref { + peek: () => T +} + /** * Checks if a value is a ref object. * @@ -54,7 +58,9 @@ export function isRef(r: any): r is Ref { */ export function ref( value: T, -): [T] extends [Ref] ? IfAny, T> : Ref, UnwrapRef | T> +): [T] extends [Ref] + ? IfAny, T> + : RefWithPeek, UnwrapRef | T> export function ref(): Ref export function ref(value?: unknown) { return createRef(value, false) @@ -66,6 +72,10 @@ export type ShallowRef = Ref & { [ShallowRefMarker]?: true } +export type ShallowRefWithPeek = ShallowRef & { + peek: () => T +} + /** * Shallow version of {@link ref}. * @@ -156,6 +166,10 @@ class RefImpl { } } } + + peek() { + return this._rawValue + } } /**