From 88f663b77073bdac660933413912957833bf570e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 10:57:01 +0800 Subject: [PATCH 01/11] feat(reactivity): support converting getter to ref with toRef() --- packages/dts-test/ref.test-d.ts | 7 +++++ packages/reactivity/__tests__/ref.spec.ts | 9 ++++++ packages/reactivity/src/ref.ts | 35 ++++++++++++++++------- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index dbf54de09c8..1ef2b966d68 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -203,6 +203,13 @@ expectType>(p2.obj.k) // Should not distribute Refs over union expectType>(toRef(obj, 'c')) + expectType>(toRef(() => 123)) + expectType>(toRef(() => obj.c)) + + const r = toRef(() => 123) + // @ts-expect-error + r.value = 234 + // toRefs expectType<{ a: Ref diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 646cc6e6791..bcfc178e952 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -287,6 +287,15 @@ describe('reactivity/ref', () => { expect(x.value).toBe(1) }) + test('toRef getter', () => { + const x = toRef(() => 1) + expect(x.value).toBe(1) + expect(isRef(x)).toBe(true) + expect(unref(x)).toBe(1) + //@ts-expect-error + expect(() => (x.value = 123)).toThrow() + }) + test('toRefs', () => { const a = reactive({ x: 1, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 85a19802d4f..888549df030 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -6,7 +6,7 @@ import { triggerEffects } from './effect' import { TrackOpTypes, TriggerOpTypes } from './operations' -import { isArray, hasChanged, IfAny } from '@vue/shared' +import { isArray, hasChanged, IfAny, isFunction } from '@vue/shared' import { isProxy, toRaw, @@ -333,6 +333,14 @@ class ObjectRefImpl { } } +class GetterRefImpl { + public readonly __v_isRef = true + constructor(private readonly _getter: () => T) {} + get value() { + return this._getter() + } +} + export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> /** @@ -362,6 +370,9 @@ export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> * @param key - Name of the property in the reactive object. * @see {@link https://vuejs.org/api/reactivity-utilities.html#toref} */ +export function toRef any>( + getter: T +): T extends () => infer R ? Readonly> : never export function toRef( object: T, key: K @@ -371,15 +382,19 @@ export function toRef( key: K, defaultValue: T[K] ): ToRef> -export function toRef( - object: T, - key: K, - defaultValue?: T[K] -): ToRef { - const val = object[key] - return isRef(val) - ? val - : (new ObjectRefImpl(object, key, defaultValue) as any) +export function toRef( + objectOrGetter: Record | (() => unknown), + key?: string, + defaultValue?: unknown +): Ref { + if (isFunction(objectOrGetter)) { + return new GetterRefImpl(objectOrGetter as () => unknown) as any + } else { + const val = objectOrGetter[key!] + return isRef(val) + ? val + : (new ObjectRefImpl(objectOrGetter, key!, defaultValue) as any) + } } // corner case when use narrows type From f6bd0346c1adf38798cedfa5bde7fdbc8150dd78 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 11:18:55 +0800 Subject: [PATCH 02/11] feat(reactivity): support getters in unref() + export MaybeRef type --- packages/dts-test/ref.test-d.ts | 1 + packages/reactivity/__tests__/ref.spec.ts | 1 + packages/reactivity/src/index.ts | 1 + packages/reactivity/src/ref.ts | 6 ++++-- packages/runtime-core/src/index.ts | 1 + 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index 1ef2b966d68..1e009205d8f 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -26,6 +26,7 @@ function plainType(arg: number | Ref) { // ref unwrapping expectType(unref(arg)) + expectType(unref(() => 123)) // ref inner type should be unwrapped const nestedRef = ref({ diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index bcfc178e952..b4f6bfdc3a6 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -196,6 +196,7 @@ describe('reactivity/ref', () => { test('unref', () => { expect(unref(1)).toBe(1) expect(unref(ref(1))).toBe(1) + expect(unref(() => 1)).toBe(1) }) test('shallowRef', () => { diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 60707febef4..2ccc07560f4 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -9,6 +9,7 @@ export { customRef, triggerRef, type Ref, + type MaybeRef, type ToRef, type ToRefs, type UnwrapRef, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 888549df030..809ac3bdf6f 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -191,6 +191,8 @@ export function triggerRef(ref: Ref) { triggerRefValue(ref, __DEV__ ? ref.value : void 0) } +export type MaybeRef = T | Ref | (() => T) + /** * Returns the inner value if the argument is a ref, otherwise return the * argument itself. This is a sugar function for @@ -207,8 +209,8 @@ export function triggerRef(ref: Ref) { * @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: T | Ref): T { - return isRef(ref) ? (ref.value as any) : ref +export function unref(ref: MaybeRef): T { + return isRef(ref) ? (ref.value as any) : isFunction(ref) ? ref() : ref } const shallowUnwrapHandlers: ProxyHandler = { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 06f9a2affd4..e2e6f0c5aa7 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -152,6 +152,7 @@ declare module '@vue/reactivity' { export { TrackOpTypes, TriggerOpTypes } from '@vue/reactivity' export type { Ref, + MaybeRef, ToRef, ToRefs, UnwrapRef, From 5cdf2ecf467362d483150a496260fcf58e553ffa Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 12:04:19 +0800 Subject: [PATCH 03/11] feat(reactivity): further expand toRef support range --- packages/dts-test/ref.test-d.ts | 39 +++++++++++++++++++++++++- packages/reactivity/src/index.ts | 1 + packages/reactivity/src/ref.ts | 45 ++++++++++++++++++++---------- packages/runtime-core/src/index.ts | 1 + 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index 1e009205d8f..a28b2408feb 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -10,7 +10,9 @@ import { toRefs, ToRefs, shallowReactive, - readonly + readonly, + MaybeRef, + MaybeReadonlyRef } from 'vue' import { expectType, describe } from './utils' @@ -327,3 +329,38 @@ describe('reactive in shallow ref', () => { expectType(x.value.a.b) }) + +describe('toRef <-> unref', () => { + function foo( + a: MaybeRef, + b: () => string, + c: MaybeReadonlyRef + ) { + const r = toRef(a) + expectType>(r) + // writable + r.value = 'foo' + + const rb = toRef(b) + expectType>>(rb) + // @ts-expect-error ref created from getter shuld be readonly + rb.value = 'foo' + + const rc = toRef(c) + expectType | Ref>>(rc) + // @ts-expect-error ref created from MaybeReadonlyRef shuld be readonly + rc.value = 'foo' + + return { + r: unref(r), + rb: unref(rb), + rc: unref(rc) + } + } + + expectType<{ + r: string + rb: string + rc: string + }>(foo('foo', () => 'bar', ref('baz'))) +}) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 2ccc07560f4..ededf1a35a7 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -10,6 +10,7 @@ export { triggerRef, type Ref, type MaybeRef, + type MaybeReadonlyRef, type ToRef, type ToRefs, type UnwrapRef, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 809ac3bdf6f..2b148872f05 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -6,7 +6,13 @@ import { triggerEffects } from './effect' import { TrackOpTypes, TriggerOpTypes } from './operations' -import { isArray, hasChanged, IfAny, isFunction } from '@vue/shared' +import { + isArray, + hasChanged, + IfAny, + isFunction, + isPlainObject +} from '@vue/shared' import { isProxy, toRaw, @@ -87,9 +93,6 @@ export function isRef(r: any): r is Ref { * @param value - The object to wrap in the ref. * @see {@link https://vuejs.org/api/reactivity-core.html#ref} */ -export function ref( - value: T -): [T] extends [Ref] ? T : Ref> export function ref(value: T): Ref> export function ref(): Ref export function ref(value?: unknown) { @@ -191,7 +194,8 @@ export function triggerRef(ref: Ref) { triggerRefValue(ref, __DEV__ ? ref.value : void 0) } -export type MaybeRef = T | Ref | (() => T) +export type MaybeRef = T | Ref +export type MaybeReadonlyRef = MaybeRef | (() => T) /** * Returns the inner value if the argument is a ref, otherwise return the @@ -209,7 +213,7 @@ export type MaybeRef = T | Ref | (() => T) * @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): T { +export function unref(ref: MaybeReadonlyRef): T { return isRef(ref) ? (ref.value as any) : isFunction(ref) ? ref() : ref } @@ -372,9 +376,12 @@ export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> * @param key - Name of the property in the reactive object. * @see {@link https://vuejs.org/api/reactivity-utilities.html#toref} */ -export function toRef any>( - getter: T -): T extends () => infer R ? Readonly> : never +// export function toRef any>( +// getter: T +// ): T extends () => infer R ? Readonly> : never +export function toRef( + value: T +): T extends () => infer R ? Readonly> : Ref> export function toRef( object: T, key: K @@ -385,17 +392,25 @@ export function toRef( defaultValue: T[K] ): ToRef> export function toRef( - objectOrGetter: Record | (() => unknown), + source: Record | MaybeRef, key?: string, defaultValue?: unknown ): Ref { - if (isFunction(objectOrGetter)) { - return new GetterRefImpl(objectOrGetter as () => unknown) as any - } else { - const val = objectOrGetter[key!] + if (isRef(source)) { + return source + } else if (isFunction(source)) { + return new GetterRefImpl(source as () => unknown) as any + } else if (isPlainObject(source) && key) { + const val = (source as Record)[key] return isRef(val) ? val - : (new ObjectRefImpl(objectOrGetter, key!, defaultValue) as any) + : (new ObjectRefImpl( + source as Record, + key, + defaultValue + ) as any) + } else { + return ref(source) } } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e2e6f0c5aa7..1e2382d1d3f 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -153,6 +153,7 @@ export { TrackOpTypes, TriggerOpTypes } from '@vue/reactivity' export type { Ref, MaybeRef, + MaybeReadonlyRef, ToRef, ToRefs, UnwrapRef, From e4397e60270bff533457d25892a03cbdd07f1e96 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 15:03:40 +0800 Subject: [PATCH 04/11] feat(reactivity): toValue --- packages/dts-test/ref.test-d.ts | 18 ++++++++++-------- packages/reactivity/src/index.ts | 3 ++- packages/reactivity/src/ref.ts | 12 ++++++++---- packages/runtime-core/src/index.ts | 3 ++- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index a28b2408feb..a897326b473 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -7,12 +7,13 @@ import { reactive, proxyRefs, toRef, + toValue, toRefs, ToRefs, shallowReactive, readonly, MaybeRef, - MaybeReadonlyRef + MaybeWritableRef } from 'vue' import { expectType, describe } from './utils' @@ -28,7 +29,8 @@ function plainType(arg: number | Ref) { // ref unwrapping expectType(unref(arg)) - expectType(unref(() => 123)) + expectType(toValue(arg)) + expectType(toValue(() => 123)) // ref inner type should be unwrapped const nestedRef = ref({ @@ -330,11 +332,11 @@ describe('reactive in shallow ref', () => { expectType(x.value.a.b) }) -describe('toRef <-> unref', () => { +describe('toRef <-> toValue', () => { function foo( - a: MaybeRef, + a: MaybeWritableRef, b: () => string, - c: MaybeReadonlyRef + c: MaybeRef ) { const r = toRef(a) expectType>(r) @@ -352,9 +354,9 @@ describe('toRef <-> unref', () => { rc.value = 'foo' return { - r: unref(r), - rb: unref(rb), - rc: unref(rc) + r: toValue(r), + rb: toValue(rb), + rc: toValue(rc) } } diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index ededf1a35a7..e7808099e60 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -3,6 +3,7 @@ export { shallowRef, isRef, toRef, + toValue, toRefs, unref, proxyRefs, @@ -10,7 +11,7 @@ export { triggerRef, type Ref, type MaybeRef, - type MaybeReadonlyRef, + type MaybeWritableRef, type ToRef, type ToRefs, type UnwrapRef, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 2b148872f05..a1c48d0d4dc 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -194,8 +194,8 @@ export function triggerRef(ref: Ref) { triggerRefValue(ref, __DEV__ ? ref.value : void 0) } -export type MaybeRef = T | Ref -export type MaybeReadonlyRef = MaybeRef | (() => T) +export type MaybeWritableRef = T | Ref +export type MaybeRef = MaybeWritableRef | (() => T) /** * Returns the inner value if the argument is a ref, otherwise return the @@ -213,8 +213,12 @@ export type MaybeReadonlyRef = MaybeRef | (() => T) * @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: MaybeReadonlyRef): T { - return isRef(ref) ? (ref.value as any) : isFunction(ref) ? ref() : ref +export function unref(ref: MaybeWritableRef): T { + return isRef(ref) ? (ref.value as any) : ref +} + +export function toValue(ref: MaybeRef): T { + return isFunction(ref) ? ref() : unref(ref) } const shallowUnwrapHandlers: ProxyHandler = { diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 1e2382d1d3f..f307c96537c 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -11,6 +11,7 @@ export { proxyRefs, isRef, toRef, + toValue, toRefs, isProxy, isReactive, @@ -153,7 +154,7 @@ export { TrackOpTypes, TriggerOpTypes } from '@vue/reactivity' export type { Ref, MaybeRef, - MaybeReadonlyRef, + MaybeWritableRef, ToRef, ToRefs, UnwrapRef, From 5ed5d27bba1cbbb05faea76b19bd7fad40f0a95a Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 15:13:46 +0800 Subject: [PATCH 05/11] feat: also check passing prop binding to toRef --- .../compileScriptPropsDestructure.spec.ts | 34 +++++++++++++++++-- packages/compiler-sfc/src/compileScript.ts | 2 +- .../src/compileScriptPropsDestructure.ts | 27 +++++++++------ 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts b/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts index 346f95a5c7a..8487270c265 100644 --- a/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScriptPropsDestructure.spec.ts @@ -294,7 +294,7 @@ describe('sfc props transform', () => { ).toThrow(`Cannot assign to destructured props`) }) - test('should error when watching destructured prop', () => { + test('should error when passing destructured prop into certain methods', () => { expect(() => compile( `` ) - ).toThrow(`"foo" is a destructured prop and cannot be directly watched.`) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to watch().` + ) expect(() => compile( @@ -313,7 +315,33 @@ describe('sfc props transform', () => { w(foo, () => {}) ` ) - ).toThrow(`"foo" is a destructured prop and cannot be directly watched.`) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to watch().` + ) + + expect(() => + compile( + `` + ) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to toRef().` + ) + + expect(() => + compile( + `` + ) + ).toThrow( + `"foo" is a destructured prop and should not be passed directly to toRef().` + ) }) // not comprehensive, but should help for most common cases diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index de9e11d071f..ec476c4ad16 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -1442,7 +1442,7 @@ export function compileScript( startOffset, propsDestructuredBindings, error, - vueImportAliases.watch + vueImportAliases ) } diff --git a/packages/compiler-sfc/src/compileScriptPropsDestructure.ts b/packages/compiler-sfc/src/compileScriptPropsDestructure.ts index bc38912653e..4ee09070d76 100644 --- a/packages/compiler-sfc/src/compileScriptPropsDestructure.ts +++ b/packages/compiler-sfc/src/compileScriptPropsDestructure.ts @@ -32,7 +32,7 @@ export function transformDestructuredProps( offset = 0, knownProps: PropsDestructureBindings, error: (msg: string, node: Node, end?: number) => never, - watchMethodName = 'watch' + vueImportAliases: Record ) { const rootScope: Scope = {} const scopeStack: Scope[] = [rootScope] @@ -152,6 +152,19 @@ export function transformDestructuredProps( return false } + function checkUsage(node: Node, method: string, alias = method) { + if (isCallOf(node, alias)) { + const arg = unwrapTSNode(node.arguments[0]) + if (arg.type === 'Identifier') { + error( + `"${arg.name}" is a destructured prop and should not be passed directly to ${method}(). ` + + `Pass a getter () => ${arg.name} instead.`, + arg + ) + } + } + } + // check root scope first walkScope(ast, true) ;(walk as any)(ast, { @@ -169,16 +182,8 @@ export function transformDestructuredProps( return this.skip() } - if (isCallOf(node, watchMethodName)) { - const arg = unwrapTSNode(node.arguments[0]) - if (arg.type === 'Identifier') { - error( - `"${arg.name}" is a destructured prop and cannot be directly watched. ` + - `Use a getter () => ${arg.name} instead.`, - arg - ) - } - } + checkUsage(node, 'watch', vueImportAliases.watch) + checkUsage(node, 'toRef', vueImportAliases.toRef) // function scopes if (isFunctionType(node)) { From 5f27eccdda6bcdda7e861de293c101b393309b2e Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 16:19:50 +0800 Subject: [PATCH 06/11] fix: toRef() & ref() on ComputedRef should return ComputedRef --- packages/dts-test/ref.test-d.ts | 27 ++++++++++++++++++++++----- packages/reactivity/src/index.ts | 2 +- packages/reactivity/src/ref.ts | 17 +++++++++++------ packages/runtime-core/src/index.ts | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index a897326b473..60f504cfcea 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -13,7 +13,9 @@ import { shallowReactive, readonly, MaybeRef, - MaybeWritableRef + MaybeRefOrGetter, + ComputedRef, + computed } from 'vue' import { expectType, describe } from './utils' @@ -334,9 +336,10 @@ describe('reactive in shallow ref', () => { describe('toRef <-> toValue', () => { function foo( - a: MaybeWritableRef, + a: MaybeRef, b: () => string, - c: MaybeRef + c: MaybeRefOrGetter, + d: ComputedRef ) { const r = toRef(a) expectType>(r) @@ -353,10 +356,16 @@ describe('toRef <-> toValue', () => { // @ts-expect-error ref created from MaybeReadonlyRef shuld be readonly rc.value = 'foo' + const rd = toRef(d) + expectType>(rd) + // @ts-expect-error ref created from computed ref shuld be readonly + rd.value = 'foo' + return { r: toValue(r), rb: toValue(rb), - rc: toValue(rc) + rc: toValue(rc), + rd: toValue(rd) } } @@ -364,5 +373,13 @@ describe('toRef <-> toValue', () => { r: string rb: string rc: string - }>(foo('foo', () => 'bar', ref('baz'))) + rd: string + }>( + foo( + 'foo', + () => 'bar', + ref('baz'), + computed(() => 'hi') + ) + ) }) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index e7808099e60..ee4da5b1935 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -11,7 +11,7 @@ export { triggerRef, type Ref, type MaybeRef, - type MaybeWritableRef, + type MaybeRefOrGetter, type ToRef, type ToRefs, type UnwrapRef, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index a1c48d0d4dc..5a337881d28 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -93,6 +93,7 @@ export function isRef(r: any): r is Ref { * @param value - The object to wrap in the ref. * @see {@link https://vuejs.org/api/reactivity-core.html#ref} */ +export function ref(value: T): T export function ref(value: T): Ref> export function ref(): Ref export function ref(value?: unknown) { @@ -194,8 +195,8 @@ export function triggerRef(ref: Ref) { triggerRefValue(ref, __DEV__ ? ref.value : void 0) } -export type MaybeWritableRef = T | Ref -export type MaybeRef = MaybeWritableRef | (() => T) +export type MaybeRef = T | Ref +export type MaybeRefOrGetter = MaybeRef | (() => T) /** * Returns the inner value if the argument is a ref, otherwise return the @@ -213,12 +214,12 @@ export type MaybeRef = MaybeWritableRef | (() => T) * @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: MaybeWritableRef): T { +export function unref(ref: MaybeRef): T { return isRef(ref) ? (ref.value as any) : ref } -export function toValue(ref: MaybeRef): T { - return isFunction(ref) ? ref() : unref(ref) +export function toValue(source: MaybeRefOrGetter): T { + return isFunction(source) ? source() : unref(source) } const shallowUnwrapHandlers: ProxyHandler = { @@ -385,7 +386,11 @@ export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> // ): T extends () => infer R ? Readonly> : never export function toRef( value: T -): T extends () => infer R ? Readonly> : Ref> +): T extends () => infer R + ? Readonly> + : T extends Ref + ? T + : Ref> export function toRef( object: T, key: K diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index f307c96537c..936d6ca3565 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -154,7 +154,7 @@ export { TrackOpTypes, TriggerOpTypes } from '@vue/reactivity' export type { Ref, MaybeRef, - MaybeWritableRef, + MaybeRefOrGetter, ToRef, ToRefs, UnwrapRef, From 781177e2d90f1a743768112e73351e4960d63b57 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 16:23:00 +0800 Subject: [PATCH 07/11] test: more toValue tests --- packages/dts-test/ref.test-d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index 60f504cfcea..852ca888bb0 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -361,6 +361,11 @@ describe('toRef <-> toValue', () => { // @ts-expect-error ref created from computed ref shuld be readonly rd.value = 'foo' + expectType(toValue(a)) + expectType(toValue(b)) + expectType(toValue(c)) + expectType(toValue(d)) + return { r: toValue(r), rb: toValue(rb), From df049425e8634683dd67742d5a34594edbc4be08 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 16:30:35 +0800 Subject: [PATCH 08/11] test: fix tests --- packages/reactivity/__tests__/ref.spec.ts | 1 - packages/reactivity/src/ref.ts | 27 ++++++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index b4f6bfdc3a6..bcfc178e952 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -196,7 +196,6 @@ describe('reactivity/ref', () => { test('unref', () => { expect(unref(1)).toBe(1) expect(unref(ref(1))).toBe(1) - expect(unref(() => 1)).toBe(1) }) test('shallowRef', () => { diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 5a337881d28..70d19054f94 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -11,7 +11,8 @@ import { hasChanged, IfAny, isFunction, - isPlainObject + isString, + isObject } from '@vue/shared' import { isProxy, @@ -316,7 +317,7 @@ export function toRefs(object: T): ToRefs { } const ret: any = isArray(object) ? new Array(object.length) : {} for (const key in object) { - ret[key] = toRef(object, key) + ret[key] = propertyToRef(object, key) } return ret } @@ -409,20 +410,24 @@ export function toRef( return source } else if (isFunction(source)) { return new GetterRefImpl(source as () => unknown) as any - } else if (isPlainObject(source) && key) { - const val = (source as Record)[key] - return isRef(val) - ? val - : (new ObjectRefImpl( - source as Record, - key, - defaultValue - ) as any) + } else if (isObject(source) && isString(key)) { + return propertyToRef(source, key, defaultValue) } else { return ref(source) } } +function propertyToRef(source: object, key: string, defaultValue?: unknown) { + const val = (source as any)[key] + return isRef(val) + ? val + : (new ObjectRefImpl( + source as Record, + key, + defaultValue + ) as any) +} + // corner case when use narrows type // Ex. type RelativePath = string & { __brand: unknown } // RelativePath extends object -> true From 5e88780ad102db2dd059d72afef28654877298c0 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 31 Mar 2023 17:30:32 +0800 Subject: [PATCH 09/11] chore: comment docs --- packages/reactivity/src/ref.ts | 45 ++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 70d19054f94..7b4d60535d1 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -219,6 +219,22 @@ export function unref(ref: MaybeRef): T { return isRef(ref) ? (ref.value as any) : ref } +/** + * Nromalizes 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): T { return isFunction(source) ? source() : unref(source) } @@ -356,9 +372,24 @@ class GetterRefImpl { export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> /** - * Can 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. + * 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 + * // requivalent 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 @@ -378,13 +409,11 @@ export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> * console.log(fooRef.value) // 3 * ``` * - * @param object - The reactive object containing the desired property. - * @param key - Name of the property in the reactive object. + * @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 any>( -// getter: T -// ): T extends () => infer R ? Readonly> : never export function toRef( value: T ): T extends () => infer R From 5691b2eb98322815a90c977dfc3e13c853008393 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 2 Apr 2023 10:09:40 +0800 Subject: [PATCH 10/11] fix: apply review feedback --- packages/dts-test/ref.test-d.ts | 6 +++--- packages/reactivity/__tests__/ref.spec.ts | 18 +++++++++++++++++- packages/reactivity/src/ref.ts | 21 ++++++++------------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/dts-test/ref.test-d.ts b/packages/dts-test/ref.test-d.ts index 852ca888bb0..bbcde45ddda 100644 --- a/packages/dts-test/ref.test-d.ts +++ b/packages/dts-test/ref.test-d.ts @@ -348,17 +348,17 @@ describe('toRef <-> toValue', () => { const rb = toRef(b) expectType>>(rb) - // @ts-expect-error ref created from getter shuld be readonly + // @ts-expect-error ref created from getter should be readonly rb.value = 'foo' const rc = toRef(c) expectType | Ref>>(rc) - // @ts-expect-error ref created from MaybeReadonlyRef shuld be readonly + // @ts-expect-error ref created from MaybeReadonlyRef should be readonly rc.value = 'foo' const rd = toRef(d) expectType>(rd) - // @ts-expect-error ref created from computed ref shuld be readonly + // @ts-expect-error ref created from computed ref should be readonly rd.value = 'foo' expectType(toValue(a)) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index bcfc178e952..718b2bc61b8 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -11,7 +11,12 @@ import { } from '../src/index' import { computed } from '@vue/runtime-dom' import { shallowRef, unref, customRef, triggerRef } from '../src/ref' -import { isShallow, readonly, shallowReactive } from '../src/reactive' +import { + isReadonly, + isShallow, + readonly, + shallowReactive +} from '../src/reactive' describe('reactivity/ref', () => { it('should hold a value', () => { @@ -275,6 +280,15 @@ describe('reactivity/ref', () => { expect(toRef(r, 'x')).toBe(r.x) }) + test('toRef on array', () => { + const a = reactive(['a', 'b']) + const r = toRef(a, 1) + expect(r.value).toBe('b') + r.value = 'c' + expect(r.value).toBe('c') + expect(a[1]).toBe('c') + }) + test('toRef default value', () => { const a: { x: number | undefined } = { x: undefined } const x = toRef(a, 'x', 1) @@ -294,6 +308,8 @@ describe('reactivity/ref', () => { expect(unref(x)).toBe(1) //@ts-expect-error expect(() => (x.value = 123)).toThrow() + + expect(isReadonly(x)).toBe(true) }) test('toRefs', () => { diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 7b4d60535d1..1bc8472115f 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -6,21 +6,15 @@ import { triggerEffects } from './effect' import { TrackOpTypes, TriggerOpTypes } from './operations' -import { - isArray, - hasChanged, - IfAny, - isFunction, - isString, - isObject -} from '@vue/shared' +import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared' import { isProxy, toRaw, isReactive, toReactive, isReadonly, - isShallow + isShallow, + ReactiveFlags } from './reactive' import type { ShallowReactiveMarker } from './reactive' import { CollectionTypes } from './collectionHandlers' @@ -220,7 +214,7 @@ export function unref(ref: MaybeRef): T { } /** - * Nromalizes values / refs / getters to values. + * 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. @@ -363,6 +357,7 @@ class ObjectRefImpl { class GetterRefImpl { public readonly __v_isRef = true + public readonly [ReactiveFlags.IS_READONLY] = true constructor(private readonly _getter: () => T) {} get value() { return this._getter() @@ -383,7 +378,7 @@ export type ToRef = IfAny, [T] extends [Ref] ? T : Ref> * toRef(() => props.foo) * * // creates normal refs from non-function values - * // requivalent to ref(1) + * // equivalent to ref(1) * toRef(1) * ``` * @@ -439,8 +434,8 @@ export function toRef( return source } else if (isFunction(source)) { return new GetterRefImpl(source as () => unknown) as any - } else if (isObject(source) && isString(key)) { - return propertyToRef(source, key, defaultValue) + } else if (isObject(source) && arguments.length > 1) { + return propertyToRef(source, key!, defaultValue) } else { return ref(source) } From dbdaefcaefe6c7ee6b3795d3668f098f358e79a1 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 2 Apr 2023 10:14:46 +0800 Subject: [PATCH 11/11] chore: avoid const enum circular dep --- packages/reactivity/src/ref.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 1bc8472115f..5dd31a9f8ca 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -13,8 +13,7 @@ import { isReactive, toReactive, isReadonly, - isShallow, - ReactiveFlags + isShallow } from './reactive' import type { ShallowReactiveMarker } from './reactive' import { CollectionTypes } from './collectionHandlers' @@ -357,7 +356,7 @@ class ObjectRefImpl { class GetterRefImpl { public readonly __v_isRef = true - public readonly [ReactiveFlags.IS_READONLY] = true + public readonly __v_isReadonly = true constructor(private readonly _getter: () => T) {} get value() { return this._getter()