From 342d2fe4c76bbac9c3359e06dbc8122e34f6f91d Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 23 Nov 2020 01:39:11 +0800 Subject: [PATCH 1/3] feat: add type-level `readonly()` api --- README.md | 12 ++++- README.zh-CN.md | 14 ++++- src/apis/index.ts | 2 + src/reactivity/index.ts | 3 +- src/reactivity/reactive.ts | 57 +------------------- src/reactivity/readonly.ts | 101 ++++++++++++++++++++++++++++++++++++ test-dts/readonly.test-d.ts | 9 ++++ 7 files changed, 137 insertions(+), 61 deletions(-) create mode 100644 src/reactivity/readonly.ts create mode 100644 test-dts/readonly.test-d.ts diff --git a/README.md b/README.md index ae736fa3..ad3d812c 100644 --- a/README.md +++ b/README.md @@ -441,6 +441,17 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar) +### `readonly` + +
+ +⚠️ readonly() provides **only type-level** readonly check. + + +`readonly()` is provided as API alignment with Vue 3 on type-level only. Use isReadonly() on it or it's properties can not be guaranteed. + +
+ ### `props`
@@ -467,7 +478,6 @@ defineComponent({ The following APIs introduced in Vue 3 are not available in this plugin. -- `readonly` - `defineAsyncComponent` - `onRenderTracked` - `onRenderTriggered` diff --git a/README.zh-CN.md b/README.zh-CN.md index f94bb80f..a60d0fa0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -405,7 +405,7 @@ watch(
-### shallowReadonly +### `shallowReadonly`
@@ -416,6 +416,17 @@ watch(
+### `readonly` + +
+ +⚠️ readonly() **只提供类型层面**的只读。 + + +`readonly()` 只在类型层面提供和 Vue 3 的对齐。在其返回值或其属性上使用 isReadonly() 检查的结果将无法保证。 + +
+ ### `props`
@@ -442,7 +453,6 @@ defineComponent({ 以下在 Vue 3 新引入的 API ,在本插件中暂不适用: -- `readonly` - `defineAsyncComponent` - `onRenderTracked` - `onRenderTriggered` diff --git a/src/apis/index.ts b/src/apis/index.ts index bf2e31e2..b5a830f0 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -21,6 +21,8 @@ export { shallowReadonly, proxyRefs, ShallowUnwrapRef, + readonly, + DeepReadonly, } from '../reactivity' export * from './lifecycle' export * from './watch' diff --git a/src/reactivity/index.ts b/src/reactivity/index.ts index f6741977..b045e915 100644 --- a/src/reactivity/index.ts +++ b/src/reactivity/index.ts @@ -5,8 +5,6 @@ export { shallowReactive, toRaw, isRaw, - isReadonly, - shallowReadonly, } from './reactive' export { ref, @@ -23,5 +21,6 @@ export { proxyRefs, ShallowUnwrapRef, } from './ref' +export { readonly, isReadonly, shallowReadonly, DeepReadonly } from './readonly' export { set } from './set' export { del } from './del' diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts index e594e535..9414051d 100644 --- a/src/reactivity/reactive.ts +++ b/src/reactivity/reactive.ts @@ -4,16 +4,12 @@ import { isPlainObject, def, warn, isArray, hasOwn, noopFn } from '../utils' import { isComponentInstance, defineComponentInstance } from '../utils/helper' import { RefKey } from '../utils/symbols' import { isRef, UnwrapRef } from './ref' -import { rawSet, accessModifiedSet, readonlySet } from '../utils/sets' +import { rawSet, accessModifiedSet } from '../utils/sets' export function isRaw(obj: any): boolean { return Boolean(obj?.__ob__ && obj.__ob__?.__raw__) } -export function isReadonly(obj: any): boolean { - return readonlySet.has(obj) -} - export function isReactive(obj: any): boolean { return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__) } @@ -219,57 +215,6 @@ export function reactive(obj: T): UnwrapRef { return observed as UnwrapRef } -export function shallowReadonly(obj: T): Readonly -export function shallowReadonly(obj: any): any { - if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) { - return obj - } - - const readonlyObj = {} - - const source = reactive({}) - const ob = (source as any).__ob__ - - for (const key of Object.keys(obj)) { - let val = obj[key] - let getter: (() => any) | undefined - let setter: ((x: any) => void) | undefined - const property = Object.getOwnPropertyDescriptor(obj, key) - if (property) { - if (property.configurable === false) { - continue - } - getter = property.get - setter = property.set - if ( - (!getter || setter) /* not only have getter */ && - arguments.length === 2 - ) { - val = obj[key] - } - } - - Object.defineProperty(readonlyObj, key, { - enumerable: true, - configurable: true, - get: function getterHandler() { - const value = getter ? getter.call(obj) : val - ob.dep.depend() - return value - }, - set(v) { - if (__DEV__) { - warn(`Set operation on key "${key}" failed: target is readonly.`) - } - }, - }) - } - - readonlySet.set(readonlyObj, true) - - return readonlyObj -} - /** * Make sure obj can't be a reactive */ diff --git a/src/reactivity/readonly.ts b/src/reactivity/readonly.ts new file mode 100644 index 00000000..2f35d66c --- /dev/null +++ b/src/reactivity/readonly.ts @@ -0,0 +1,101 @@ +import { reactive, Ref, UnwrapRef } from '.' +import { isArray, isPlainObject, warn } from '../utils' +import { readonlySet } from '../utils/sets' +import { isRef } from './ref' + +export function isReadonly(obj: any): boolean { + return readonlySet.has(obj) +} + +type Primitive = string | number | boolean | bigint | symbol | undefined | null +type Builtin = Primitive | Function | Date | Error | RegExp + +// prettier-ignore +export type DeepReadonly = T extends Builtin + ? T + : T extends Map + ? ReadonlyMap, DeepReadonly> + : T extends ReadonlyMap + ? ReadonlyMap, DeepReadonly> + : T extends WeakMap + ? WeakMap, DeepReadonly> + : T extends Set + ? ReadonlySet> + : T extends ReadonlySet + ? ReadonlySet> + : T extends WeakSet + ? WeakSet> + : T extends Promise + ? Promise> + : T extends {} + ? { readonly [K in keyof T]: DeepReadonly } + : Readonly + +// only unwrap nested ref +type UnwrapNestedRefs = T extends Ref ? T : UnwrapRef + +/** + * **In @vue/composition-api, `reactive` only provides type-level readonly check** + * + * Creates a readonly copy of the original object. Note the returned copy is not + * made reactive, but `readonly` can be called on an already reactive object. + */ +export function readonly( + target: T +): DeepReadonly> { + if (isRef(target)) { + return target as any + } + return shallowReadonly(target) as any +} + +export function shallowReadonly(obj: T): Readonly +export function shallowReadonly(obj: any): any { + if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) { + return obj + } + + const readonlyObj = {} + + const source = reactive({}) + const ob = (source as any).__ob__ + + for (const key of Object.keys(obj)) { + let val = obj[key] + let getter: (() => any) | undefined + let setter: ((x: any) => void) | undefined + const property = Object.getOwnPropertyDescriptor(obj, key) + if (property) { + if (property.configurable === false) { + continue + } + getter = property.get + setter = property.set + if ( + (!getter || setter) /* not only have getter */ && + arguments.length === 2 + ) { + val = obj[key] + } + } + + Object.defineProperty(readonlyObj, key, { + enumerable: true, + configurable: true, + get: function getterHandler() { + const value = getter ? getter.call(obj) : val + ob.dep.depend() + return value + }, + set(v) { + if (__DEV__) { + warn(`Set operation on key "${key}" failed: target is readonly.`) + } + }, + }) + } + + readonlySet.set(readonlyObj, true) + + return readonlyObj +} diff --git a/test-dts/readonly.test-d.ts b/test-dts/readonly.test-d.ts new file mode 100644 index 00000000..c128e47b --- /dev/null +++ b/test-dts/readonly.test-d.ts @@ -0,0 +1,9 @@ +import { readonly } from './index' + +describe('should support DeepReadonly', () => { + const r = readonly({ obj: { k: 'v' } }) + // @ts-expect-error + r.obj = {} + // @ts-expect-error + r.obj.k = 'x' +}) From e604afbdb801f05552c4df7672fbdfad14dfb541 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 25 Nov 2020 09:42:12 +0800 Subject: [PATCH 2/3] Update src/reactivity/readonly.ts --- src/reactivity/readonly.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/reactivity/readonly.ts b/src/reactivity/readonly.ts index 2f35d66c..fae4a764 100644 --- a/src/reactivity/readonly.ts +++ b/src/reactivity/readonly.ts @@ -43,10 +43,7 @@ type UnwrapNestedRefs = T extends Ref ? T : UnwrapRef export function readonly( target: T ): DeepReadonly> { - if (isRef(target)) { - return target as any - } - return shallowReadonly(target) as any + return target as any } export function shallowReadonly(obj: T): Readonly From 060c0dcb07ed9e20a0ecc953302cd76f4c545920 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Thu, 3 Dec 2020 10:38:56 +0800 Subject: [PATCH 3/3] chore: update tests --- src/reactivity/readonly.ts | 1 - test-dts/readonly.test-d.ts | 55 +++++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/reactivity/readonly.ts b/src/reactivity/readonly.ts index fae4a764..10ca2416 100644 --- a/src/reactivity/readonly.ts +++ b/src/reactivity/readonly.ts @@ -1,7 +1,6 @@ import { reactive, Ref, UnwrapRef } from '.' import { isArray, isPlainObject, warn } from '../utils' import { readonlySet } from '../utils/sets' -import { isRef } from './ref' export function isReadonly(obj: any): boolean { return readonlySet.has(obj) diff --git a/test-dts/readonly.test-d.ts b/test-dts/readonly.test-d.ts index c128e47b..1ca26a56 100644 --- a/test-dts/readonly.test-d.ts +++ b/test-dts/readonly.test-d.ts @@ -1,9 +1,48 @@ -import { readonly } from './index' - -describe('should support DeepReadonly', () => { - const r = readonly({ obj: { k: 'v' } }) - // @ts-expect-error - r.obj = {} - // @ts-expect-error - r.obj.k = 'x' +import { expectType, readonly, ref } from './index' + +describe('readonly', () => { + it('nested', () => { + const r = readonly({ + obj: { k: 'v' }, + arr: [1, 2, '3'], + objInArr: [{ foo: 'bar' }], + }) + + // @ts-expect-error + r.obj = {} + // @ts-expect-error + r.obj.k = 'x' + + // @ts-expect-error + r.arr.push(42) + // @ts-expect-error + r.objInArr[0].foo = 'bar2' + }) + + it('with ref', () => { + const r = readonly( + ref({ + obj: { k: 'v' }, + arr: [1, 2, '3'], + objInArr: [{ foo: 'bar' }], + }) + ) + + console.log(r.value) + + expectType(r.value.obj.k) + + // @ts-expect-error + r.value = {} + + // @ts-expect-error + r.value.obj = {} + // @ts-expect-error + r.value.obj.k = 'x' + + // @ts-expect-error + r.value.arr.push(42) + // @ts-expect-error + r.value.objInArr[0].foo = 'bar2' + }) })