From cc326a72d2db2a4e5a26e960903906865905381f Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Tue, 6 May 2025 13:30:32 +0300 Subject: [PATCH 1/9] feat: ref peek --- .../reactivity/__tests__/computed.spec.ts | 50 +++++++++++++++++++ packages/reactivity/__tests__/ref.spec.ts | 22 ++++++++ packages/reactivity/src/computed.ts | 8 +++ packages/reactivity/src/ref.ts | 16 +++++- 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c7963499f85..e70565f062c 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__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 7976a5373ba..e2530d9b5fe 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 ea798e201d4..eec4545f932 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/ref.ts b/packages/reactivity/src/ref.ts index 59b713dd862..738ad8f5cf4 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 + } } /** From d089f23943f3db1868ff6b6ed480157dd8f8222e Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Tue, 6 May 2025 13:31:01 +0300 Subject: [PATCH 2/9] feat: untrack --- packages/reactivity/__tests__/effect.spec.ts | 12 ++++++++++++ packages/reactivity/src/effect.ts | 9 +++++++++ 2 files changed, 21 insertions(+) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 242fc707153..63e93de9ad8 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/src/effect.ts b/packages/reactivity/src/effect.ts index 84cf184c154..1c6e6cb84eb 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 From acf02a198a9cfadb1f56d236f27f3ce8f6959ba2 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Tue, 6 May 2025 13:44:27 +0300 Subject: [PATCH 3/9] feat: add untrack export --- packages/reactivity/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index f0445e87da0..9bc9b8af342 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, From dc89e00ee6162367097d5464c3d995461f898760 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Wed, 23 Jul 2025 16:12:04 +0300 Subject: [PATCH 4/9] chore: remove peek --- .../reactivity/__tests__/computed.spec.ts | 50 ------------------- packages/reactivity/__tests__/ref.spec.ts | 22 -------- packages/reactivity/src/computed.ts | 8 --- packages/reactivity/src/ref.ts | 16 +----- 4 files changed, 1 insertion(+), 95 deletions(-) diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index e70565f062c..c7963499f85 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -1150,54 +1150,4 @@ 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__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index e2530d9b5fe..7976a5373ba 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -534,26 +534,4 @@ 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 eec4545f932..ea798e201d4 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -26,11 +26,9 @@ interface BaseComputedRef extends Ref { export interface ComputedRef extends BaseComputedRef { readonly value: T - peek: () => T } export interface WritableComputedRef extends BaseComputedRef { - peek: () => T [WritableComputedRefSymbol]: true } @@ -153,12 +151,6 @@ 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/ref.ts b/packages/reactivity/src/ref.ts index 738ad8f5cf4..59b713dd862 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -34,10 +34,6 @@ export interface Ref { [RefSymbol]: true } -export interface RefWithPeek extends Ref { - peek: () => T -} - /** * Checks if a value is a ref object. * @@ -58,9 +54,7 @@ export function isRef(r: any): r is Ref { */ export function ref( value: T, -): [T] extends [Ref] - ? IfAny, T> - : RefWithPeek, UnwrapRef | T> +): [T] extends [Ref] ? IfAny, T> : Ref, UnwrapRef | T> export function ref(): Ref export function ref(value?: unknown) { return createRef(value, false) @@ -72,10 +66,6 @@ export type ShallowRef = Ref & { [ShallowRefMarker]?: true } -export type ShallowRefWithPeek = ShallowRef & { - peek: () => T -} - /** * Shallow version of {@link ref}. * @@ -166,10 +156,6 @@ class RefImpl { } } } - - peek() { - return this._rawValue - } } /** From 87e0d1dc640ce83feb20ac7cf83a7b04114b61a1 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Wed, 23 Jul 2025 16:12:29 +0300 Subject: [PATCH 5/9] feat(runtime-core): add untrack --- packages/runtime-core/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 9910f82102b..f6c7aab6d56 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -27,6 +27,7 @@ export { toRaw, // effect effect, + untrack, stop, getCurrentWatcher, onWatcherCleanup, From ec0a97eabe6d640caff5db2aabeb220d882fe210 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Wed, 23 Jul 2025 17:00:04 +0300 Subject: [PATCH 6/9] chore: revert auto format --- packages/runtime-core/__tests__/rendererFragment.spec.ts | 2 +- packages/runtime-core/src/compat/componentAsync.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererFragment.spec.ts b/packages/runtime-core/__tests__/rendererFragment.spec.ts index 81cf7b8df31..3394a41b2a0 100644 --- a/packages/runtime-core/__tests__/rendererFragment.spec.ts +++ b/packages/runtime-core/__tests__/rendererFragment.spec.ts @@ -416,7 +416,7 @@ describe('renderer: fragment', () => { const root = nodeOps.createElement('div') const renderFn = () => { - return openBlock(true), createBlock(Fragment, null) + return (openBlock(true), createBlock(Fragment, null)) } render(renderFn(), root) diff --git a/packages/runtime-core/src/compat/componentAsync.ts b/packages/runtime-core/src/compat/componentAsync.ts index b6bd6dd4b94..fc6ae3b7939 100644 --- a/packages/runtime-core/src/compat/componentAsync.ts +++ b/packages/runtime-core/src/compat/componentAsync.ts @@ -35,7 +35,7 @@ export function convertLegacyAsyncComponent( let resolve: (res: LegacyAsyncReturnValue) => void let reject: (reason?: any) => void const fallbackPromise = new Promise((r, rj) => { - ;(resolve = r), (reject = rj) + ;((resolve = r), (reject = rj)) }) const res = comp(resolve!, reject!) From 82914dc2ee4a02fb6dcd7cf44179031f40b40fca Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Wed, 23 Jul 2025 17:05:36 +0300 Subject: [PATCH 7/9] test(runtime-core): add untrack for computed --- packages/reactivity/__tests__/effect.spec.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index d8f47331f4f..75b108b2da5 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -1183,14 +1183,19 @@ describe('reactivity/effect', () => { }) it('should not track dependencies when using untrack', () => { - const value = ref(1) - let dummy + const a = ref(1) + const b = computed(() => a.value) + let dummyA + let dummyB effect(() => { - dummy = untrack(() => value.value) + dummyA = untrack(() => a.value) + dummyB = untrack(() => b.value) }) - expect(dummy).toBe(1) - value.value = 2 - expect(dummy).toBe(1) + expect(dummyA).toBe(1) + expect(dummyB).toBe(1) + a.value = 2 + expect(dummyA).toBe(1) + expect(dummyB).toBe(1) }) describe('dep unsubscribe', () => { From 3973e6b24cc8ca17626bdffe275e22e21dbf78a2 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Wed, 23 Jul 2025 17:06:24 +0300 Subject: [PATCH 8/9] chore: revert auto format --- .../runtime-core/__tests__/rendererAttrsFallthrough.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts index 45d00579fee..73e67aab2a0 100644 --- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts @@ -573,7 +573,7 @@ describe('attribute fallthrough', () => { const Child = { props: [], render() { - return openBlock(), createBlock('div') + return (openBlock(), createBlock('div')) }, } From bece02d17e3988887c4141bec3346cdd8f3ad427 Mon Sep 17 00:00:00 2001 From: teleskop150750 Date: Wed, 23 Jul 2025 17:29:33 +0300 Subject: [PATCH 9/9] refactor(runtime-core): [untrack] use setActiveSub --- packages/reactivity/src/effect.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 76700f1dd1a..120cccb5dae 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -285,11 +285,12 @@ export function cleanup( } export function untrack(fn: () => T): T { + if (!activeSub) return fn() + const prevSub = setActiveSub() try { - pauseTracking() return fn() } finally { - resetTracking() + setActiveSub(prevSub) } }